Developer / APIMarketplace API

Marketplace API

The Marketplace is the catalogue of products a tenant can buy individually. The free core gives every tenant an entitlement baseline; catalog products raise that baseline (an extra branch, an e-Fatura integration, advanced reports, AI credits, and so on). There are no plans, tiers, or bundles: every paid capability is sold with the annual licence (license_annual) as its prerequisite.

Buying a product writes a TenantAddOn row, and the entitlement projector folds that row’s grants map into the tenant’s existing entitlement set. The combine rule depends on the key prefix:

PrefixTypeCombineExample
limit.*numberSUMFree baseline limit.maxBranches: 1 + 2× extra_branch (+1 each) = 3
feature.*booleanORIf any source sets feature.advancedReports: true → enabled
integration.*string[]UNIONfiscal_efatura + fiscal_huginintegration.fiscal: ["efatura", "hugin"]

For limit.*, -1 is the “unlimited” sentinel: if any source grants -1, the result is unlimited and summing stops (adding capacity to an already-unlimited cap is a no-op).

All paths are under the /api global prefix. Prices are in TRY and stored in the catalogue as cents (priceCents): 490000 = ₺4,900.00. Amounts are shown KDV-inclusive (gross). For base URL, auth realms, the error envelope, and rate-limit tiers, see API Fundamentals.

⚠️

There is no tenant-facing “free grant” endpoint. The legacy POST /v1/marketplace/addons/purchase was removed (deep-review C2): it was guarded only by @Roles(ADMIN) and called purchase() with no paymentRef, so any owner could activate a paid product for free via curl. The paid checkout flow is now mandatory; operator comps are issued only from the superadmin surface.

Endpoints

MethodPathAuthRolesNotes
GET/v1/marketplace/addonsPublicCatalogue; published rows only
GET/v1/marketplace/addons/availableStaff JWTADMIN, MANAGERCatalogue annotated with includedInPlan for the caller’s entitlements
GET/v1/marketplace/addons/mineStaff JWTADMIN, MANAGERProducts the tenant currently holds
POST/v1/checkout/intentStaff JWTADMIN, MANAGERMixed-cart checkout intent (the only buy path)
DELETE/v1/marketplace/addons/:idStaff JWTADMINCancel a TenantAddOn (period-end or immediate)

These are tenant-level endpoints: they do not expect X-Branch-Id. The checkout intent and the cancel endpoint move/settle money or entitlements and are protected accordingly.

To render a storefront in a single call, prefer GET /v1/me/licensing: it returns owned products, credit balances, the offer for every entitlement key priced for this tenant today, and a purchasability verdict per product. See the Licensing & Billing API.

List the catalogue

The catalogue is public (it also shows on the landing site); no authentication is required. Only published products are returned, and each row is trimmed for the UI.

# All published products
curl https://hummytummy.com/api/v1/marketplace/addons
 
# Filter by kind: license | module | integration | capacity | credit | service
curl "https://hummytummy.com/api/v1/marketplace/addons?kind=integration"

Response:

[
  {
    "code": "module_inventory",
    "name": "Stok & Maliyet Yönetimi",
    "description": "Reçete, stok sayımı, satın alma siparişi, fire takibi, tedarikçi yönetimi ve şubeler arası transfer.",
    "kind": "module",
    "billing": "annual",
    "priceCents": 390000,
    "currency": "TRY",
    "deps": [],
    "requiresLicense": true,
    "creditKind": null,
    "creditUnits": null,
    "maxQuantity": null,
    "sortOrder": 11,
    "i18n": { "tr": { "name": "…", "description": "…" }, "en": { "…": "…" } }
  }
]

The grants field is not returned in the public catalogue — only the superadmin surface and the catalog source file carry the grant map. The catalogue is trimmed for the UI; priceCents/currency are the source of truth for price. The prorated price for this tenant today comes from offers[...].proratedCents in GET /v1/me/licensing.

Catalogue summary

Every sellable product at a glance. The Grants column is shown here for reference only; it is not part of the public catalogue payload. For the details, see the Entitlement Matrix.

CodeKindBillingPriceGrantsLicenceDependency
license_annuallicenseannual₺4,900 (490000)feature.license, feature.prioritySupport, integration.fiscal += efatura
advanced_reportsmoduleannual₺1,290 (129000)feature.advancedReports
module_inventorymoduleannual₺3,900 (390000)feature.inventoryTracking
module_reservationsmoduleannual₺990 (99000)feature.reservationSystem
module_personnelmoduleannual₺990 (99000)feature.personnelManagement
module_personnel_card_shiftmoduleone-time₺4,000 (400000)feature.cardShiftmodule_personnel
module_ai_studiomoduleannual₺1,990 (199000)feature.aiContentGeneration
api_accessmoduleannual₺2,490 (249000)feature.apiAccess
module_external_displaymoduleannual₺1,990 (199000)feature.externalDisplay
priority_supportmoduleannualfeature.prioritySupportarchived v3.6.7 — folded into license_annual
delivery_platformsintegrationannual₺2,499 (249900)integration.delivery += yemeksepeti, getir, trendyol_yemek, migros; feature.deliveryIntegration
delivery_yemeksepetiarchived in v3.6.8 — folded into delivery_platforms
delivery_getirarchived in v3.6.8 — folded into delivery_platforms
delivery_trendyol_yemekarchived in v3.6.8 — folded into delivery_platforms
fiscal_efaturaintegrationannualintegration.fiscal += efaturaarchived v3.6.7 — folded into license_annual
fiscal_huginintegrationannual₺2,990 (299000)integration.fiscal += hugin
caller_id_integrationintegrationannual₺1,490 (149000)integration.caller += generic
sms_integrationintegrationannual₺990 (99000)integration.sms += *
extra_branchcapacityannual₺3,990 (399000)limit.maxBranches +1, feature.multiLocation
credit_ai_photo_100creditone-time₺690 (69000)— (100 PHOTO credits)module_ai_studio
credit_ai_video_20creditone-time₺890 (89000)— (20 VIDEO credits)module_ai_studio
credit_ai_3d_10creditone-time₺790 (79000)— (10 MODEL3D credits)module_ai_studio
credit_sms_500creditone-time₺490 (49000)— (500 SMS credits)sms_integration
onsite_install_fullserviceone-time₺7,500 (750000)— (grants: {}, service line)
⚠️

onsite_install_full is a service line: it is paid and invoiced but grants no entitlement (grants: {}). It is one-time, not recurring, and gets no period-end window (currentPeriodEnd is null). Credit packs carry no grants either — they top up a balance and produce a CreditLot row, not a TenantAddOn.

extra_branch carries both a limit (SUM) and a feature (OR) grant in a single purchase: { "limit.maxBranches": 1, "feature.multiLocation": true }. The multi-branch UI is already free; what is paid for is the second branch itself. All three delivery_* products write to integration.delivery and are union-ed, and the feature.deliveryIntegration flag alongside them folds with OR.

Retired codes (kds_extra_screen, kds_extra_station, extra_tablet) are archived, never deleted: MarketplaceAddOn.code may not be reused and TenantAddOn.addOnId is onDelete: Restrict. All three granted limit.kdsScreens / limit.kdsStations / limit.tablets — keys no enforcement code has ever read — and the à-la-carte model drops device-capacity pricing entirely.

Buy a product (checkout intent)

The only way to buy is the checkout (PayTR) rail. A paid product is never provisioned without proof of payment (paymentRef).

Start a checkout intent

Submit the selected products (plus any hardware / service lines) as a single mixed cart to POST /v1/checkout/intent. The server re-prices the cart (it never trusts client totals), mints a paymentRef of the form CK-<uuid7>, freezes the cart onto a CheckoutIntent row, and returns a PayTR iframe token + payment link.

curl -X POST https://hummytummy.com/api/v1/checkout/intent \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "cart": {
      "items": [
        { "type": "addon", "code": "license_annual", "qty": 1 },
        { "type": "addon", "code": "fiscal_hugin", "qty": 1 }
      ]
    },
    "buyer": {
      "email": "[email protected]",
      "name": "Ali Veli",
      "phone": "+905551112233",
      "address": "..."
    },
    "returnUrl": "https://app.hummytummy.com/checkout/done",
    "acceptedDocumentIds": ["<KVKK>", "<DISTANCE_SALES>", "<REFUND>"]
  }'

Pay via PayTR

The buyer pays on the PayTR hosted iframe. Each cart line is shown to the buyer as a separate basket entry (e.g. “e-Fatura (Nilvera) (yıllık)”).

Settlement via webhook

The PayTR webhook carries only merchant_oid + total_amount. The dispatcher sees the CK- prefix and routes the callback to CheckoutSettlementService. There the CheckoutIntent is found, the frozen cart is read, and CheckoutService.confirmAndProvision runs.

Grants are provisioned

For each catalog line, confirmAndProvision calls TenantMarketplaceService.purchase() with the settled paymentRef inside the checkout transaction. That creates a TenantAddOn row (or reactivates a lapsed one in place) and appends an AddOnPurchased event to the outbox. The entitlement projector consumes AddOnPurchased and folds the product’s grants map into the tenant’s entitlement set; guards and the UI then see the new limit/feature/integration.

Purchasability and dependencies (deps)

Every addon line passes the purchasability gate before any charge. A rejected line returns 409 and stops the whole cart:

codeMeaning
LICENSE_REQUIREDThe product needs a licence and the cart has none either
ADDON_ALREADY_OWNEDThis product (or the licence) is already active
ADDON_ALREADY_GRANTEDEverything the product grants is already in the entitlement set
ADDON_REQUIRES_DEPENDENCYIts dependency is neither owned nor in the cart
ADDON_LIMIT_REDUNDANTThe capacity it adds is already unlimited
ADDON_MAX_QUANTITYThe catalog ceiling would be exceeded (100 for extra_branch)

Every entry in deps is a catalog product code and must be satisfied at purchase time by an active ownership row or by a sibling line in the same cart. Today the products carrying deps are the four credit packs (credit_ai_*module_ai_studio, credit_sms_500sms_integration).

⚠️

The legacy plan:<NAME> form (e.g. fiscal_hugin’s plan:PRO) is gone along with plans. Catalog validation now rejects any plan:-prefixed dep at write time: Tenant.currentPlanId is NULL for every tenant, so such a dep could never be satisfied and would block every sale of that product.

Duplicate-purchase guard

Buying the same product twice and gaining nothing is blocked: if an active row already exists for the same (tenantId, addOnId, branchId), the second purchase is rejected with ADDON_ALREADY_OWNED. Capacity products are bought by the unit, so their ceiling is maxQuantity instead. Credit packs are consumable and are never blocked. Two concurrent purchases are caught by a Serializable transaction; the loser gets a retryable 409 (the card is not charged twice).

Idempotency

PayTR retries aggressively (it retries even after a 200 OK if the body isn’t "OK"/"FAIL"). There are multiple layers of protection against double provisioning:

CheckoutIntent.status check

If the intent is already provisioned/failed, nothing is touched.

confirmAndProvision is independently idempotent

It is idempotent on (tenantId, paymentRef).

purchase() returns the existing row

For the same paymentRef it returns the existing TenantAddOn row without re-emitting an AddOnPurchased event.

My current products

Lists the rows the tenant currently holds.

curl https://hummytummy.com/api/v1/marketplace/addons/mine \
  -H "Authorization: Bearer $JWT"

Each row carries the product catalogue (addOn) plus its status/period fields:

[
  {
    "id": "ta_01J...",
    "tenantId": "t_01J...",
    "addOnId": "ao_01J...",
    "branchId": null,
    "quantity": 2,
    "pendingQuantity": null,
    "status": "active",
    "activatedAt": "2026-08-13T10:00:00.000Z",
    "currentPeriodStart": "2026-08-13T00:00:00.000Z",
    "currentPeriodEnd": "2027-03-10T00:00:00.000Z",
    "cancelAtPeriodEnd": false,
    "paymentRef": "CK-018f...",
    "chargedCents": 434300,
    "currency": "TRY",
    "origin": "purchase",
    "addOn": { "code": "extra_branch", "name": "Ek Şube", "...": "..." }
  }
]
FieldMeaning
statusactive · past_due (grace window, entitlement still live) · cancelled · expired
quantity / pendingQuantityCurrent units / units to apply at the next anniversary (capacity downgrades are renewal-time)
chargedCentsWhat was actually charged (prorated) for this period; 0 for comps
originpurchase · comp · migration

Annual products run to the account’s anniversary, not to a rolling window — currentPeriodEnd is the same date on every row, so the whole account renews on one date with one invoice. One-time items (onsite_install_full) carry currentPeriodEnd: null.

Cancel a product

Cancellation is ADMIN-only (it is a billing decision). By default the row is cancelled at period end; pass ?immediate=true to revoke right away.

curl -X DELETE \
  https://hummytummy.com/api/v1/marketplace/addons/$TENANT_ADDON_ID \
  -H "Authorization: Bearer $JWT"

The row is set to cancelAtPeriodEnd: true and stays active. The limit/feature/integration entitlements it grants keep living until the nightly sweeper / renewal-cycle close transitions the row to cancelled.

Only active and past_due rows can be cancelled; anything else returns 400. For a past_due row the cancellation is always immediate — its paid period has already ended, so deferring to period end would only keep the grace grant alive with no way to revoke it.

⚠️

Two concurrent cancel calls converge on a single transition (a status-scoped updateMany). The loser gets Cancel raced with another request — refresh and retry; the AddOnCancelled event is not emitted twice.

  • Entitlement Matrix — the free core, product grants, licence suppression, and the ENTITLEMENT_REQUIRED envelope.
  • Licensing & Billing API — the checkout rail, proration, the anniversary, invoices, and renewals.
  • API Fundamentals — base URL, auth realms, error envelope, rate-limit tiers.
  • Webhooks — receive outbound events (the api_access module grants feature.apiAccess).
  • Operator experience (browsing, buying, and tracking products in the panel): Marketplace help.