Nano Store - Ecommerce Docs
Developer ResourcesCheckout

Checkout API Reference

Complete API contracts for the Checkout module, including routes, auth, DTOs, responses, errors, examples, and integration notes.

Checkout - API Reference

Audience: Frontend engineers, mobile engineers, backend engineers, QA, and API consumers. Scope: The five customer checkout routes and the four admin session routes. Payment has no HTTP surface.

1. Documentation Evidence

AreaFiles InspectedWhat Was Verified
Controllersapps/api/src/modules/checkout/customer/checkout-customer.controller.ts, admin/checkout-admin.controller.tsRoutes, methods, guards, permissions, rate limits
DTOsdto/*.tsValidation, strict unknown-property rejection
Servicesshared/checkout-session.service.ts, checkout-payment.service.ts, checkout-release.service.tsBehavior, errors
Schemapackages/db/src/schema/checkout/*.tsTotals CHECK, partial uniques, snapshots
Error registryapps/api/src/common/types/error-codes.ts (// CHECKOUT)CHECKOUT_* codes

2. Module Summary

FieldValue
Module namecheckout
Module slugcheckout
Primary actorscustomer, admin, payment (future, internal)
API surfacesmobile (customer), admin
Base route prefixes/api/mobile/checkout, /api/checkout/sessions
Auth modelJwtAuthGuard (customer); JwtAuthGuard + RoleGuard (admin)
PersistencePostgreSQL (three session tables + snapshots); no cache
Runtime source of truthcheckout_session rows + live inventory/promotion ledgers
Sibling docsBackend, Features and flows

3. Concepts and Terminology

TermMeaningSource FileUsed By
attemptThe payment-attempt handle; every payment-side transition is scoped to itschemaPayment contract
expiresAt / expiresInSecondsSession expiry; a read predicate, never a stored statusschemaAll routes
statuspending_payment / payment_in_progress / completed / cancelled / expiredschemaAll routes
grandTotalsubtotal - discount + shipping - shippingDiscount — database-enforcedschemaResponse
reservedfalse on a line = untracked product, nothing to hold — normalsession serviceItems
priceChangeWarning, not a blocker — the cart's price vs the frozen pricesession serviceItems
cartVersionOptional optimistic token for the basket you displayedsession serviceStart
readinessOn the quote only — ready/blockingReasons in place of a 409, so a client can render a disabled state with a reasoncheckout-quote.service.tsQuote
expectedGrandTotalThe figure the customer was shown; the pay route refuses when the server computes a different onecheckout-session.service.tsPay

4. API Surface Map

SurfaceMethodPathActorAuth/GuardPermissionControllerPurpose
MobilePOST/api/mobile/checkout/quoteCustomerJWT + IpThrottle—CheckoutCustomerControllerPrice the basket, writes nothing
MobilePOST/api/mobile/checkoutCustomerJWT + IpThrottle—sameStart checkout
MobilePOST/api/mobile/checkout/payCustomerJWT + IpThrottle—sameFreeze the basket and start paying, in one request
MobileGET/api/mobile/checkout/activeCustomerJWT + IpThrottle—sameMy live session
MobilePOST/api/mobile/checkout/:id/cancelCustomerJWT + IpThrottle—sameCancel my pending session
AdminGET/api/checkout/sessionsAdminJWT+RoleCheckout_READCheckoutAdminControllerList sessions
AdminGET/api/checkout/sessions/:idAdminJWT+RoleCheckout_READsameSession detail
AdminPOST/api/checkout/sessions/:id/cancelAdminJWT+RoleCheckout_UPDATEsameCancel (fraud/duplicate)
AdminPOST/api/checkout/sessions/:id/expireAdminJWT+RoleCheckout_UPDATEsameForce-expire (stuck gateway)

{id} is always a uuid7 public_id; no integer PK is exposed.

5. Auth, Identity, and Permissions

SurfaceGuard/DecoratorIdentity ShapePermissionGuest AllowedNotes
CustomerJwtAuthGuard, IpThrottlerGuardreq.user.id—NoCUSTOMER_CHECKOUT_QUOTE 40/min account-keyed (quote — writes nothing), CUSTOMER_CHECKOUT_ATTEMPT 10/min account-keyed (start — a checkout is a money-bearing attempt), CUSTOMER_CHECKOUT_PAY 10/min account-keyed (pay — freezes the basket and reaches a gateway), CUSTOMER_READ 60/min (active), CUSTOMER_WRITE 20/min (cancel)
AdminJwtAuthGuard, RoleGuard, IpThrottlerGuardreq.userCheckout_READ / Checkout_UPDATENoADMIN_READ 30/min, ADMIN_WRITE 10/min

Another customer's session and a session that does not exist both return 404 with the same code — a 403 would confirm the id is real. Do not distinguish them.

6. DTO and Model Reference

6.1 StartCheckoutDto

FieldTypeRequiredValidationNotes
addressIdUUID v7Yes@IsUUID("7")
couponCodestringNo3–64 chars
cartVersionnumberNo>= 1Send when you have one — a stale version is a 409, not a silent repurchase

Do not send prices, discounts, shipping or totals. The server recalculates all of them, and the request is rejected outright if any unknown property is present — not silently stripped.

6.1a QuoteCheckoutDto

FieldTypeRequiredValidationNotes
fulfilmentTypedelivery | pickupNo@IsInDefaults to delivery
addressIdUUID v7No@IsUUID("7")Required for a delivery, refused for a pickup
couponCodestringNotrimmed + upper-cased, promotion module's min/max lengthCompared against a stored code, unlike StartCheckoutDto's

No cartVersion — a quote writes nothing, so there is no lost update to guard, and requiring it would fail the first quote a client makes, before it has read the cart at all.

6.1b PayCheckoutDto

The same four fields StartCheckoutDto carries, plus:

FieldTypeRequiredValidationNotes
methodpayment method enumYes@IsInHow the customer is paying
expectedGrandTotalinteger, minor unitsYes@IsInt, @Min(0)The total the customer was shown. The request is refused if the server computes a different one. @Min(0), not @Min(1) — a fully discounted pickup basket legitimately costs nothing

6.2 Params DTO

CheckoutSessionParamsDto { id } — uuid7.

6.3 Admin query DTO

status, customerId, createdFrom, createdTo, minTotal, maxTotal, offset pagination (page/size), sort by createdAt / updatedAt / expiresAt / grandTotal.

6.4 Response DTOs

Start/active/detail share the session shape:

{
  "checkout": { "id": "0198f2c1-…", "status": "pending_payment",
                "expiresAt": "…", "expiresInSeconds": 900, "version": 1 },
  "pricing": { "currency": "NPR", "subtotal": 250000, "discountAmount": 20000,
               "shippingAmount": 15000, "shippingDiscountAmount": 0, "grandTotal": 245000,
               "anyPriceChanged": true },
  "items": [{ "id": "…", "productId": "…", "name": "…", "sku": "ABC-1",
              "unitPrice": 100000, "mrp": 120000, "quantity": 2, "lineTotal": 200000,
              "priceChange": { "changed": true, "previousPrice": 95000, "currentPrice": 100000 },
              "reserved": true }],
  "promotions": [{ "id": "…", "promotionId": "…", "name": "Festival 20%",
                   "couponCode": "SAVE20", "discountAmount": 20000, "shippingDiscountAmount": 0 }],
  "shipping": { "districtName": "Kathmandu", "municipalityName": "…", "ward": 5,
                "street": "…", "landmark": null, "postalCode": null,
                "recipientName": "…", "recipientPhone": "+977…", "fee": 15000 },
  "payment": { "ready": true, "amountDue": 245000, "currency": "NPR" }
}

All money is integer minor units (245000 = NPR 2,450.00) — never parse as float. grandTotal is database-enforced; display any part without recomputing.

6.4a Quote response — CheckoutQuoteResponseDto

{
  "pricing": { "currency": "NPR", "subtotal": 89000, "discountAmount": 5000,
               "shippingAmount": 15000, "shippingDiscountAmount": 0, "grandTotal": 99000 },
  "shipping": { "fulfilmentType": "delivery", "serviceable": true, "fee": 15000,
                "districtName": "Kathmandu" },
  "coupon": { "code": "SAVE20", "applied": true, "discountAmount": 5000,
              "shippingDiscountAmount": 0, "message": "…" },
  "readiness": { "ready": true, "blockingReasons": [], "liveSessionId": null,
                 "liveSessionStatus": null }
}

coupon is null when the request carried no coupon code. shipping.fee is null exactly when shipping.serviceable is false; shipping.districtName is always null for a pickup. readiness.ready is exactly readiness.blockingReasons.length === 0. readiness.liveSessionId / liveSessionStatus are non-null when the customer already has a live session, so a client can offer a release rather than showing a blocked cart with no action.

7. Enum Reference

EnumValueMeaningRuntime EffectSource
checkout_session_statuspending_paymentCreated; holds takenCancellable; payment may startenums.ts
checkout_session_statuspayment_in_progressAt the gatewaySweep-exempt; not cancellable from the app
checkout_session_statuscompleted / cancelled / expiredTerminalexpired is derived from expires_at — the status is only written to record the release
readiness.blockingReasons (quote)empty / cart_locked / items_unavailable / insufficient_stock / address_not_serviceableWhy this basket cannot be committed yetIts own vocabulary, one wider than the cart's — the cart has no address to be unserviceablecheckout.constants.ts (CHECKOUT_QUOTE_BLOCKING_REASONS)

8. Endpoint Reference

8.0a POST /api/mobile/checkout/quote

Purpose

Price the basket for a chosen address and coupon without writing anything — no session, no stock hold, no promotion hold, no cart lock, no transaction. Everything the create path THROWS for as a business refusal (unserviceable district, inapplicable coupon, locked cart, unavailable items) comes back here as readiness and coupon data instead, so a client can show a disabled state with a sentence rather than an error. POST for the body, not for the effect — a coupon code does not belong in a query string, and the response must never be cached, since it is a price.

Auth and Permissions

JwtAuthGuard; CUSTOMER_CHECKOUT_QUOTE 40/min (account-keyed) — four times the pay budget, because this writes nothing, holds nothing and takes no lock.

Request

{ "fulfilmentType": "delivery", "addressId": "0198f2c1-…", "couponCode": "SAVE20" }

All fields optional; fulfilmentType defaults to delivery.

Response

200. Shape per §6.4a.

Side Effects

None.

Error Cases

What it still refuses is malformed or unauthorised input, not a business condition — those come back as readiness/coupon data with a 200.

HTTPCodeCondition
400CHECKOUT_ADDRESS_NOT_ALLOWED_FOR_PICKUPPickup carrying an address
400CHECKOUT_ADDRESS_REQUIRED_FOR_DELIVERYDelivery carrying no address
404CHECKOUT_ADDRESS_NOT_FOUNDThe address is unknown, archived, or not theirs

8.1 POST /api/mobile/checkout

Purpose

Turn the cart into a validated, price-frozen purchase attempt that holds stock. 201 on create; 200 when an existing live session was returned.

Auth and Permissions

JwtAuthGuard; CUSTOMER_CHECKOUT_ATTEMPT 10/min (account-keyed).

Request

{ "addressId": "0198f2c1-…", "couponCode": "SAVE20", "cartVersion": 7 }

Prices, discounts, shipping and totals must not be sent — the server recalculates; any unknown property rejects the request.

Response

201 (create) or 200 (existing live session — the response is identical either way, so a repeat POST, retry after timeout, or two tabs are all safe). Session shape per §6.4.

Side Effects

One transaction: inventory + promotion reservations, price/shipping/promotion freeze, session + snapshot rows. A failed checkout leaves nothing behind.

Error Cases

HTTPCodeCondition
400validationUnknown property sent (rejected, not stripped)
404CHECKOUT_ADDRESS_NOT_FOUNDUnknown, archived, or not theirs
409CHECKOUT_CART_EMPTYNo cart or no lines
409CHECKOUT_CART_CHANGEDcartVersion stale
409CHECKOUT_CART_NOT_READYBlocking reasons — details.blockingReasons
409CHECKOUT_PRODUCT_UNAVAILABLEdetails.unavailableVariantIds — VARIANT public ids, not product ids: two configurations of one product can have only one unavailable
409CHECKOUT_INSUFFICIENT_STOCKdetails.shortfalls[] — {productId, variantId, requested, available}; stock is held per variant, so variantId is the identifier that matters
409CHECKOUT_ADDRESS_NOT_SERVICEABLENo delivery to that district
409CHECKOUT_COUPON_NOT_APPLICABLEdetails.reason — retry without the coupon is a valid recovery

CHECKOUT_CART_NOT_READY, CHECKOUT_PRODUCT_UNAVAILABLE and CHECKOUT_INSUFFICIENT_STOCK are one builder (checkoutNotReady) choosing between three codes by precedence — every one of them carries all three detail fields (blockingReasons, unavailableVariantIds, shortfalls), not only the one its name suggests. A client can read whichever fields are non-empty regardless of which code came back.

The five things that shape the UI

  1. A repeat POST returns the SAME session with a 200, not an error — no button guard needed.
  2. expiresInSeconds is server-computed — run the countdown from it, never from the device clock. At zero, re-fetch: the session reports status: "expired".
  3. priceChange.changed is a warning, not a blocker — show "the price of X changed from A to B"; the customer has not paid yet.
  4. reserved: false on a line is normal — the product is not stock-tracked; nothing to hold.
  5. status is already correct for expiry — never compare expiresAt yourself; a session past expiry reads expired regardless of any background job.

8.1a POST /api/mobile/checkout/pay

Purpose

Freeze the basket and start the payment, in one request. Revalidates the cart, holds the stock, freezes the amount payable and starts the payment. This is what closes the gap the old two-step flow left open: POST /checkout used to be fired from the review step, minutes before payment, so the cart sat locked and stock held while the customer was still deciding how to pay. Both routes still exist and still work — this route exists so the freeze and the payment start happen together.

Auth and Permissions

JwtAuthGuard; CUSTOMER_CHECKOUT_PAY 10/min (account-keyed).

Request

The StartCheckoutDto fields plus method and expectedGrandTotal — DTO per §6.1b.

{ "addressId": "0198f2c1-…", "couponCode": "SAVE20", "method": "esewa", "expectedGrandTotal": 99000 }

Response

200 — PaymentResponseDto, the same shape POST /api/payments returns.

Side Effects

The create transaction's usual holds (inventory, promotion, session, snapshot rows), plus starting the payment attempt. No compensating write on a payment-start failure: create refuses inside its own transaction and rolls back cleanly when the total or the destination does not match, leaving no session, no hold and an untouched cart. A failure inside the payment start is repaired by PaymentAttemptService itself, which returns the session to pending_payment with its holds intact so the customer can press Pay again — see backend §6.x PaymentAttemptService.

Error Cases

HTTPCodeCondition
400validationUnknown property, or a pickup/delivery address mismatch
404CHECKOUT_ADDRESS_NOT_FOUNDUnknown, archived, or not theirs
409CHECKOUT_TOTAL_CHANGEDThe server's computed total differs from expectedGrandTotal — rolled back, nothing held
409CHECKOUT_SESSION_MISMATCHA live session already exists for a different address/total/cart version than this request asked for
409CHECKOUT_CART_EMPTY / CHECKOUT_CART_CHANGED / CHECKOUT_CART_NOT_READYSame conditions as POST /checkout
409CHECKOUT_PRODUCT_UNAVAILABLE / CHECKOUT_INSUFFICIENT_STOCKSame conditions as POST /checkout
409CHECKOUT_ADDRESS_NOT_SERVICEABLENo delivery to that district
409CHECKOUT_COUPON_NOT_APPLICABLERetry without the coupon is a valid recovery
409CHECKOUT_PAYMENT_IN_PROGRESSA payment for this account is already at a gateway. If the resumed session's fulfilment/address/cart-version/total match this request, it is handed to the payment module to heal or refuse; otherwise this code is returned

CHECKOUT_SESSION_MISMATCH exists specifically for two tabs pressing Pay with different addresses: equal totals do not make it safe to resume the other tab's session, because two addresses in the same district can cost the same and are not the same place.

8.2 GET /api/mobile/checkout/active

Purpose

Fetch my live session (for countdown polling or resume). CUSTOMER_READ 60/min.

Response

200 — session shape. An expired session reads expired; the customer starts again.

8.3 POST /api/mobile/checkout/:id/cancel

Purpose

Cancel my own pending session. Releases the held stock and the coupon slot, unlocks the cart. CUSTOMER_WRITE 20/min.

Error Cases

HTTPCodeCondition
404CHECKOUT_SESSION_NOT_FOUNDUnknown session, or not theirs (same code for both)
409CHECKOUT_SESSION_EXPIREDIt lapsed — offer to start again
409CHECKOUT_SESSION_NOT_CANCELLABLEAlready terminal — refresh
409CHECKOUT_PAYMENT_IN_PROGRESSA payment is at the gateway — tell them to finish or contact support

8.4 GET /api/checkout/sessions

Purpose

Admin session list. Checkout_READ; ADMIN_READ 30/min. Filters: status, customerId, createdFrom, createdTo, minTotal, maxTotal; offset pagination; sort by createdAt/updatedAt/expiresAt/grandTotal.

8.5 GET /api/checkout/sessions/:id

Detail. 404 CHECKOUT_SESSION_NOT_FOUND.

8.6 POST /api/checkout/sessions/:id/cancel

Purpose

Cancel a pending_payment session an operator judges fraudulent or duplicated. Releases holds + coupon slot, unlocks the cart, records which administrator acted. Checkout_UPDATE; ADMIN_WRITE 10/min.

8.7 POST /api/checkout/sessions/:id/expire

Purpose

Force-expire a session stuck at a payment gateway that never called back — the only lever for that state, because the automatic sweep deliberately never touches payment_in_progress. Same release semantics as cancel, with the administrator recorded.

9. Flow Diagrams

9.1 Route Ownership

9.2 Request Sequence (start)

9.3 Error Branch (start)

EndpointPagination TypeDefault SizeMax SizeSort FieldsFiltersResult Cap
GET /api/checkout/sessionsoffset page/size20100createdAt, updatedAt, expiresAt, grandTotalstatus, customerId, createdFrom, createdTo, minTotal, maxTotal—

Customer endpoints are single-session (never paginated).

11. Caching, Jobs, and External Integrations

IntegrationUsed?Details
Redis cacheNo — checkout caches nothingEvery read is live or frozen
BullMQYescheckout queue — the expiry sweep, enqueued by a plain cron scheduler (no outbox row, no accompanying DB write)
External APINoPayment is a separate module, not yet built

13. Mandatory Deep API Documentation Pack

13.1 Route-by-Route Completeness Matrix

RouteController MethodDTOsService MethodGuardsPermissionsCacheJobsDB TouchesErrorsDocumented?
POST /api/mobile/checkout/quotequoteQuoteCheckoutDtoCheckoutQuoteService.quoteJWT+IpThrottle———cart, address, promotion (reads only)400/404Yes
POST /api/mobile/checkoutcreateCreateCheckoutDtoCheckoutSessionService.createJWT+IpThrottle———cart, address, inventory, promotion, session + snapshots400/404/409 (11 codes)Yes
POST /api/mobile/checkout/paypayPayCheckoutDtoCheckoutPayService.pay → CheckoutSessionService.create → PaymentCustomerService.startJWT+IpThrottle———cart, address, inventory, promotion, session + snapshots, payment attempt400/404/409 (13 codes)Yes
GET /api/mobile/checkout/activegetActive—…getActiveJWT+IpThrottle———sessions—Yes
POST /:id/cancelcancelparams DTO…cancelJWT+IpThrottle———session + releases404/409Yes
GET /api/checkout/sessionsfindAllquery DTOCheckoutAdminService.listJWT+Role+IpThrottleCheckout_READ——sessions—Yes
GET /api/checkout/sessions/:idfindByIdparams DTO…findOneJWT+Role+IpThrottleCheckout_READ——session404Yes
POST /:id/cancel (admin)cancelparams DTOCheckoutReleaseService.releaseAndCloseJWT+Role+IpThrottleCheckout_UPDATE——session + releases + audit404/409Yes
POST /:id/expireexpireparams DTO…releaseAndCloseJWT+Role+IpThrottleCheckout_UPDATE——session + releases + audit404/409Yes

13.2 Request/Response Exhaustiveness

Covered in §8: minimal start request (§6.1/8.1), full session response (§6.4), the 200-on-repeat behavior (§8.1 point 1), the expiry-read-predicate behavior (§8.1 point 5), domain errors per endpoint (§8 error tables), the same-404-for-others/unknown rule (§5), rate-limit behavior (10/min checkout budget).

13.3 API Diagram Pack

Route ownership (§9.1), request sequence (§9.2, backend §7.1), error decision tree (§9.3), payment-transition flow (backend §7.2 — no HTTP surface).

13.4 Consumer Integration Notes

ConsumerRequired KnowledgeFailure HandlingContract Stability
Web frontend200-on-repeat, server expiresInSeconds, priceChange warning, reserved:false normal409 codes → specific UI; coupon retry-without is validStable
Mobile appCUSTOMER_CHECKOUT_ATTEMPT 10/min; countdown from server429 → back off; re-fetch at zeroStable
Admin panelCancel vs expire semantics; read-only otherwise409 NOT_CANCELLABLE / PAYMENT_IN_PROGRESS → refreshStable
QAExpiry read predicate, sweep exemption, attempt scopingReproduce via exact codesStable
Payment (future)markPaymentStarted/complete/fail contract; own the failure edgesAttempt-scoped transitionsStable

13.5 API Tradeoffs and Rationale

DecisionChosen BehaviorAlternatives ConsideredWhy This TradeoffRiskMitigation
One transactionAtomic everythingSaga/coordinatorNo orphan stateModules must accept DbExecutorAlready true
200-on-repeatSame session409 duplicateDouble-click safeAmbiguityIdentical response
Expiry as predicateAlways correctStored status + flip jobNo stale pay-for-stock window—Sweep writes only to record
Sweep exempts gatewayNo unfulfillable chargeSweep everythingStuck sessionsSilent gatewayAdmin force-expire
Attempt-scoped transitionsSafe retriesIdempotency onlyCyclic machine—Attempt number
Snapshots everywhereImmutable historyReferencesReceipt integrityStorageAccepted
Admin read-onlyContract integrityAdmin editNo repriced contractsCan't fix errorsCancel/expire only
Quote writes nothingNo session, no holds, no lock, no transactionReuse create and release on cancelSidebar render never enters the reservation lock convoyQuote and commit can disagree on a promotion right at its usage limitCommit path re-checks and refuses inside its own transaction
Pay is one route, not twocreate + payment start in one requestKeep the two-step flowCloses the gap where a locked cart sat waiting on payment method choiceTotal can move between quote and pressexpectedGrandTotal refuses a mismatch and rolls back

13.6 API Change Impact

ChangeAffected ConsumersBackend ImpactData ImpactMigration Needed?Compatibility Plan
Payment module landsCheckout clientsCalls markPaymentStarted/complete/failNoneNoContract already frozen + tested

14. Zero-Omission API Checklist

  • Every controller route is documented (§4, §8, §13.1).
  • Every parent route prefix and runtime URL is documented (§2, §4).
  • Every DTO field, enum, default, transform and validator is documented (§6, §7).
  • Every response field and nullable field is documented (§6.4, §8).
  • Every auth, guard, permission and guest identity branch is documented (§5).
  • Every success, validation, not-found, conflict, rate-limit and server-error branch is documented (§8).
  • Every DB read/write, queue job and external call is documented (§11, backend §9).
  • Every route has examples for minimal request, success response and representative failures (§8).
  • Every endpoint family has route, sequence and error diagrams (§9, backend §7).
  • Every tradeoff and compatibility risk is documented (§13.5, §13.6).
  • The API doc links to backend and features/flows (§1, See Also).

15. Integration Checklist

  • Every route from controllers is documented.
  • Every DTO field is documented.
  • Every enum value is documented.
  • Every response envelope is documented.
  • Every error code is documented.
  • Every auth guard and permission is documented.
  • Every cache key, queue job and external call is documented.
  • Every diagram matches the current code.
  • The API doc links to backend and features/flows.

See Also