Documentation/Building Your App/Transactional Services
Transactional Services
Prebuilt backend capabilities — cart, order, payment, booking, membership, and more — with a real state machine, idempotency guarantees, and a single wire primitive: POST /api/v1/services/execute.
Overview
Transactional services are prebuilt backend capabilities you opt into by extending your own schema from a platform base schema — instead of hand-rolling a cart, order, or booking state machine yourself. Each service ships a schema (with a real lifecycle/status field), a state machine that enforces legal transitions, and a small set of operations (verbs) that mutate a record safely: validated input, state guards, idempotency, and an audit trail.
You still read and list your records with ordinary CRUD. The only new thing transactional services add is how you mutate lifecycle state: instead of PUT-ing a new value onto a status field, you call an operation, and the platform enforces whether that transition is legal from the record's current state.
🚨 Never write a lifecycle field directly
Do not PUT status (or any other lifecycle field) on a transactional record directly. It bypasses input validation, the state machine, and the audit trail, and can leave related records (e.g. order totals, cart items) out of sync. Always call the operation.
The Model: extends + one wire primitive
You opt in by declaring extends on your own schema, pointing at a platform base schema in "<service>:base_<name>" form — see the Schema Reference for how extends resolves. Your schema inherits the base's fields (status, timestamps, computed totals, etc.) plus whatever fields you add.
json
{
"name": "luxe_cart",
"parent_type": "tenant",
"extends": "cart:base_cart",
"attributes": [
{ "name": "gift_wrap", "type": "boolean", "default-value": false }
]
}
{
"name": "luxe_order",
"parent_type": "tenant",
"extends": "order:base_order",
"attributes": []
}Once a service is imported into your project (via the services catalog — see Managed Integrations & Provider Catalog), every lifecycle mutation on that entity goes through exactly one wire primitive:
http
POST https://api.supero.dev/api/v1/services/execute
X-API-Key: ak_...
Content-Type: application/json
{
"service_id": "cart",
"operation": "checkout_cart",
"input": {
"cart_uuid": "5b6e...",
"extending_schema": "myapp:luxe_cart",
"cart_item_extending_schema": "myapp:luxe_cart_item",
"order_extending_schema": "myapp:luxe_order",
"order_item_extending_schema": "myapp:luxe_order_item"
},
"domain": "mydomain",
"project_uuid": "8f2a..."
}json
{
"success": true,
"service_id": "cart",
"operation": "checkout_cart",
"output": {
"record_uuid": "5b6e...",
"order_uuid": "9c1d...",
"status": "checked_out",
"checked_out_at": "2026-07-18T09:12:03Z"
},
"side_effects": ["created myapp:luxe_order 9c1d..."],
"record_uuid": "5b6e...",
"error": null
}Reading and listing the underlying records (a cart, an order, a booking) is ordinary CRUD: GET/PUT/DELETE https://api.supero.dev/api/v1/crud/{domain}/{type}[/{uuid}], rich filtered reads via POST .../crud/{domain}/query, and bulk fetches via POST .../crud/{domain}/list-bulk. Only the lifecycle mutation goes through /services/execute.
ℹ️ extending_schema tells the platform which of your schemas to operate on
Because multiple app schemas can extend the same base (your own cart plus, say, an imported billing integration's own customer schema), every operation input includes the qualified name(s) of your extending schema(s) — extending_schema, and for two-schema services like cart/order/approval/inventory/loyalty_points/document_signature, a second slot (e.g. cart_item_extending_schema). Register these once (see Client Access below) instead of repeating them on every call.
Service Catalog
Discover what's available and already imported at runtime with GET https://api.supero.dev/api/v1/services/catalog (summary of every service) or GET .../api/v1/services/catalog/{service_id} for one service's full schema/operation detail. The catalog below groups the ~23 transactional services by the kind of app they support.
Commerce & fulfillment
| Service | What it models | Base schema(s) |
|---|---|---|
| cart | A shopping cart and its line items. | cart:base_cart, cart:base_cart_item |
| order | An order created from a cart (or standalone, for invoice-style apps). | order:base_order, order:base_order_item |
| payment | The Supero-side record of a payment as it moves through authorize → capture → refund/void. Not a payment processor itself — pair it with a gateway like stripe_checkout or billing_razorpay. | payment:base_payment |
| inventory | Stock-keeping: total/available quantity per SKU, plus reservations held until commit or release. | inventory:base_inventory, inventory:base_reservation |
Payment gateways (services catalog connectors)
| Service | What it does |
|---|---|
| stripe_checkout | Hosted Stripe checkout sessions and payment links: create_checkout_session, retrieve_checkout_session, create_payment_link, expire_session, deactivate_payment_link, get_publishable_config. |
| billing_razorpay | UPI/cards/wallets/net-banking via Razorpay: create_order, create_refund. |
Scheduling
| Service | What it models | Base schema |
|---|---|---|
| appointment | A provider-based visit (clinics, salons, consultants). | appointment:base_appointment |
| booking | A time-slot reservation against a resource (rooms, equipment, classes). | booking:base_booking |
| rental | An asset taken out for a date/time range and returned — priced by duration × rate, with a security deposit. Distinct from booking (the asset leaves the premises). | rental:base_rental |
Membership & subscriptions
| Service | What it models | Base schema |
|---|---|---|
| membership | Role-bearing org/team/group membership (member of X) — distinct from billing. | membership:base_membership |
| recurring_plan | A recurring-billing plan (SaaS subscriptions, retainers, gym memberships). Named recurring_plan because "subscription" is a reserved schema name. | recurring_plan:base_plan |
| loyalty_points | A points balance plus a transaction ledger for accrual/redemption/expiry. | loyalty_points:base_points_account, loyalty_points:base_points_txn |
Catalog primitives
| Service | What it models | Base schema |
|---|---|---|
| product | A tangible, carted/ordered good. | product:base_product |
| service | An intangible, performed offering (a haircut, a consult, a rental day-rate) — sold via cart/order and/or scheduled via appointment/booking. | service:base_service |
| customer | The commerce-side profile linked to a platform user account. Identity fields live on the user account; this holds commerce fields. | customer:base_customer |
Workflow, support & content
| Service | What it models | Base schema |
|---|---|---|
| task | Lightweight TODO tracking (done/not-done, no resolution stages). | task:base_task |
| ticket | Support tickets / helpdesk cases with an assignee and priority. | ticket:base_ticket |
| approval | Single-step or sequential multi-step approval flows (expense reports, PTO, procurement). | approval:base_approval, approval:base_approval_step |
| document_signature | Multi-party document signing (contracts, NDAs, consent forms) — not for invoices/receipts. | document_signature:base_document, document_signature:base_signature |
| reviews | Customer reviews with a moderation lifecycle, verified-purchase identity, and business responses. | reviews:base_review |
| feedback | Ratings/surveys attached to any record. | feedback:base_feedback |
| comment | Threaded comments on any record. | comment:base_comment |
| attachment | File-attachment metadata and its own upload/scan/archive lifecycle. | attachment:base_attachment |
| notification | In-app notification records (queue → sent → seen/dismissed) — distinct from the email/SMS connector services. | notification:base_notification |
💡 Let Supero pick services for you
When you describe your app in plain language, Supero's app generator can select an appropriate starting set of services for you (e.g. a salon booking app gets service + appointment + payment). You can always add, remove, or reconfigure services afterward from the Services page in your project — see Build Your First App (App Studio) and Managed Integrations & Provider Catalog.
Per-Journey Operation Maps
The operations below are the actual verbs each service exposes. Call them through the client access patterns in the next section, or raw via the wire primitive above.
Commerce: cart → checkout → order → pay → fulfill
| Step | Operation | Key input | Notes |
|---|---|---|---|
| Add to cart | cart.add_to_cart | product_uuid, quantity, unit_price | Creates the Customer + open Cart if they don’t exist yet, and upserts the line. Calling it again for the same product increases quantity — it is cumulative, not idempotent. |
| Recompute totals | cart.recompute_totals | cart_uuid | Safe to call anytime; naturally idempotent. |
| Checkout | cart.checkout_cart | cart_uuid (+ your cart/cart_item/order/order_item extending schemas) | open → checked_out (terminal). Creates a pending Order. Calling it again on an already-checked-out cart returns the same order — safe to retry. |
| Confirm order | order.place_order | order_uuid | pending → confirmed. |
| Charge | stripe_checkout.create_checkout_session or billing_razorpay.create_order | success_url / amount (major units) | Creates a hosted payment session; mirror the result onto a payment record with payment.authorize_payment → payment.capture_payment. |
| Fulfill | order.fulfill_order | order_uuid | confirmed → fulfilled. |
| Cancel | order.cancel_order | order_uuid, cancellation_reason | pending/confirmed → cancelled. |
Booking: check availability → confirm
| Step | Operation | Key input | Notes |
|---|---|---|---|
| Check availability | booking.check_availability / appointment.check_provider_availability | resource_uuid, resource_field, start_time, end_time | Advisory only — an overlap check, not a hold. A slot can still be taken between checking and confirming, so handle a 409 on confirm as a normal race, not a bug. |
| Confirm | booking.book_slot / appointment.schedule_appointment | booking_uuid or appointment_uuid | requested → confirmed. booking.book_slot also supports a create-and-confirm call shape (pass resource_uuid + start_time + end_time instead of an existing uuid). |
| Reschedule | booking.reschedule_booking / appointment.reschedule_appointment | start_time, end_time | Field-only update — not idempotent; don’t auto-retry. |
| Complete / no-show | booking.complete_booking, appointment.complete_appointment / mark_no_show | booking_uuid or appointment_uuid | confirmed → completed or no_show (terminal). |
Subscriptions, membership & loyalty
| Journey | Operations | Notes |
|---|---|---|
| Subscribe / manage a plan | recurring_plan.start_plan → pause_plan / resume_plan / cancel_plan | inactive → active → paused → cancelled/expired. There is no built-in recurring-charge scheduler — billing ticks (charge + renew_period) are something your app composes on a schedule. |
| Membership | membership.enroll_member → suspend_member / reinstate_member / expel_member / transfer_member | pending → active → suspended/expelled/transferred. |
| Loyalty points | loyalty_points.accrue_points / redeem_points / expire_points / reverse_txn | redeem_points 409s if the balance is less than the amount requested. |
State Machines & Error Handling
Every transactional base schema ships a declared state machine: an initial state, a set of states (some terminal), and named transitions, each restricted to a from set of allowed source states.
| Service | Initial state | States |
|---|---|---|
| cart | open | open → checked_out (terminal) | abandoned | expired |
| order | pending | pending → confirmed → fulfilled (terminal) | cancelled (terminal) | refunded (terminal) |
| payment | pending | pending → authorized → captured → refunded (terminal) | partially_refunded | voided (terminal) | failed (terminal) |
| appointment / booking | requested | requested → confirmed → completed (terminal) | cancelled (terminal) | no_show (terminal) |
| membership | pending | pending → active ⇄ suspended → expelled (terminal) | transferred (terminal) | expired (terminal) |
| recurring_plan | inactive | inactive ⇄ active ⇄ paused → cancelled (terminal) | expired (terminal) |
Calling an operation that isn't legal from the record's current state does not raise a transport-level error — the HTTP status of the /services/execute call itself stays 200. Check the success and error_code fields in the response body, not the transport status:
json
{
"success": false,
"service_id": "cart",
"operation": "checkout_cart",
"output": {
"error_code": "INVALID_STATE_TRANSITION",
"error": "Cannot checkout_cart from state 'checked_out'. Allowed from: ['open'].",
"current_state": "checked_out",
"current_state_terminal": true,
"attempted_operation": "checkout_cart",
"allowed_from_states": ["open"],
"target_state": "checked_out",
"remediation": {
"type": "terminal_state",
"message": "Record is in terminal state 'checked_out'; no further operations are valid.",
"available_operations_from_current_state": []
}
},
"error": "Cannot checkout_cart from state 'checked_out'. Allowed from: ['open'].",
"status_code": 409,
"error_code": "INVALID_STATE_TRANSITION"
}remediation.available_operations_from_current_state tells you exactly which operations are legal right now — use it to drive which action buttons your UI shows, instead of hardcoding a transition map client-side.
💡 Discover legal transitions ahead of time
GET https://api.supero.dev/api/v1/services/transactional-catalog returns every imported service's operations, idempotency categories, and full state machine (states, terminal flags, allowed transitions) in one call — useful for building a generic "what can I do to this record" UI without hardcoding per-service logic.
Idempotency Guidance
Not every operation is safe to retry the same way. Each operation falls into one of three categories — know which one you're calling before you decide to retry on a timeout or 5xx.
| Category | Meaning | Safe to auto-retry? | Examples |
|---|---|---|---|
| State-guarded (idempotent) | Retrying after the transition already happened returns the same end state (often with a reason field signaling "already done") instead of erroring or duplicating. | Yes | cart.checkout_cart, cart.get_or_create_open_cart, order.place_order, order.fulfill_order, booking.book_slot, payment.authorize_payment, payment.capture_payment |
| Naturally idempotent | The operation recomputes a derived value; calling it twice is harmless because it doesn’t depend on prior calls. | Yes | cart.recompute_totals, order.recompute_order_totals, task.block_task / unblock_task, ticket.assign_ticket |
| Unsafe to retry (cumulative) | Each call has an additive or one-way effect — a blind retry double-counts or double-charges. | No — check first, or use a client-supplied dedup key | cart.add_to_cart (quantity grows on repeat calls), payment.refund_payment, recurring_plan.renew_period, inventory.adjust_quantity, approval.approve_step / reject_step |
For cart.add_to_cart specifically, pass a stable client_line_id when you want a retry to be treated as the same add intent rather than a second line — the server dedups exactly on that value, so retries must resend the same id every time.
⚠️ Prefer a pre-check over a blind retry on Category 3 ops
Before retrying refund_payment, re-read the payment record and check its status is still captured/partially_refunded — a payment already in refunded means the earlier call succeeded and the response just didn't reach you. The SDKs' built-in retry logic already treats these operations as non-retryable for exactly this reason.
Client Access
Two supported ways to call transactional operations, depending on where your code runs.
From your app’s browser frontend
Register your app's extending schema(s) for each service once — the client injects them into every call so you never repeat qualified schema names — then call operations through the per-service accessor.
javascript
client.transactional.registerExtensions({
cart: {
extending_schema: 'myns:luxe_cart',
cart_item_extending_schema: 'myns:luxe_cart_item',
customer_extending_schema: 'myns:luxe_customer' // required for add_to_cart
},
order: { extending_schema: 'myns:luxe_order', order_item_extending_schema: 'myns:luxe_order_item' }
});
await client.transactional.cart.addToCart({ productUuid: sku, quantity: 1, unitPrice: 42.0 });
await client.transactional.cart.checkout(cartUuid);
await client.transactional.order.fulfill(orderUuid, 'shipped via courier');
// Raw escape hatch — any service/operation not covered by a typed accessor
await client.transactional.execute('loyaltyPoints', 'accrue_points', { txn_uuid: txnUuid });A failed call rejects with a typed error you can branch on: TxnStateConflictError (409 — check err.currentState / err.isStateTransitionError), TxnNotFoundError (404), TxnBadRequestError (400/422), TxnPlatformError (502 — transient), TxnSoftFailureError (a 200 response with success:false).
From server-side Python (scripts, jobs, backend integrations)
bash
pip install superopython
from supero.service_lib import ServiceLib
sl = ServiceLib(api_key='ak_...', domain='mydomain', project_uuid='8f2a...')
# or ServiceLib.from_env() reading SUPERO_URL / SUPERO_API_KEY / SUPERO_DOMAIN / SUPERO_PROJECT_UUID
sl.transactional.register_extensions({
'cart': ('myns:luxe_cart', 'myns:luxe_cart_item'), # two-schema services take a tuple
'order': ('myns:luxe_order', 'myns:luxe_order_item'),
'payment': 'myns:luxe_payment', # single-schema services take a string
})
result = sl.cart.checkout(cart_uuid) # ServiceResult — dict-like, plus .record_uuid / .status / .reason
sl.order.fulfill(result['order_uuid'], fulfillment_notes='shipped via courier')
try:
sl.payment.refund(payment_uuid, refund_amount=30.00)
except StateConflictError as e:
# already refunded, or not yet captured — inspect e.message
passThe Python errors mirror the JS ones exactly: BadRequestError (400), NotFoundError (404), StateConflictError (409), PlatformError (502 — the SDK auto-retries this one up to 3 times for state-guarded and naturally-idempotent operations only), and SoftFailureError (200 with success:false). All import from supero.transactional (also re-exported from supero.service_lib).
ℹ️ Two-schema services
cart, order, approval, inventory, loyalty_points, and document_signature each need two extending schemas registered (a header and a line/step/item schema) — pass a tuple/array in that order, or the explicit two-key object form shown in the JS example above.
Next steps
- •Schema Reference — the full fields[]/references[]/extends schema format used by every base schema in this catalog.
- •Managed Integrations & Provider Catalog — how to browse, import, and configure a service into your project before you can call it.
- •Managing Data — CRUD, rich queries, and bulk reads for the records these operations create.
- •Anatomy of a Supero App — where transactional services fit into a generated app’s schema, config, and UI files.
- •Workflows & Automation — chaining these operations into declarative multi-step flows.
- •Subscriptions & Memberships — the recurring_plan and membership services in depth.
On this page