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:
| Prefix | Type | Combine | Example |
|---|---|---|---|
limit.* | number | SUM | Free baseline limit.maxBranches: 1 + 2× extra_branch (+1 each) = 3 |
feature.* | boolean | OR | If any source sets feature.advancedReports: true → enabled |
integration.* | string[] | UNION | fiscal_efatura + fiscal_hugin → integration.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
| Method | Path | Auth | Roles | Notes |
|---|---|---|---|---|
GET | /v1/marketplace/addons | Public | — | Catalogue; published rows only |
GET | /v1/marketplace/addons/available | Staff JWT | ADMIN, MANAGER | Catalogue annotated with includedInPlan for the caller’s entitlements |
GET | /v1/marketplace/addons/mine | Staff JWT | ADMIN, MANAGER | Products the tenant currently holds |
POST | /v1/checkout/intent | Staff JWT | ADMIN, MANAGER | Mixed-cart checkout intent (the only buy path) |
DELETE | /v1/marketplace/addons/:id | Staff JWT | ADMIN | Cancel 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.
| Code | Kind | Billing | Price | Grants | Licence | Dependency |
|---|---|---|---|---|---|---|
license_annual | license | annual | ₺4,900 (490000) | feature.license, feature.prioritySupport, integration.fiscal += efatura | — | — |
advanced_reports | module | annual | ₺1,290 (129000) | feature.advancedReports | ✅ | — |
module_inventory | module | annual | ₺3,900 (390000) | feature.inventoryTracking | ✅ | — |
module_reservations | module | annual | ₺990 (99000) | feature.reservationSystem | ✅ | — |
module_personnel | module | annual | ₺990 (99000) | feature.personnelManagement | ✅ | — |
module_personnel_card_shift | module | one-time | ₺4,000 (400000) | feature.cardShift | ✅ | module_personnel |
module_ai_studio | module | annual | ₺1,990 (199000) | feature.aiContentGeneration | ✅ | — |
api_access | module | annual | ₺2,490 (249000) | feature.apiAccess | ✅ | — |
module_external_display | module | annual | ₺1,990 (199000) | feature.externalDisplay | ✅ | — |
priority_support | module | annual | — | feature.prioritySupport | archived v3.6.7 — folded into license_annual | — |
delivery_platforms | integration | annual | ₺2,499 (249900) | integration.delivery += yemeksepeti, getir, trendyol_yemek, migros; feature.deliveryIntegration | ✅ | — |
delivery_yemeksepeti | — | — | archived in v3.6.8 — folded into delivery_platforms | — | — | — |
delivery_getir | — | — | archived in v3.6.8 — folded into delivery_platforms | — | — | — |
delivery_trendyol_yemek | — | — | archived in v3.6.8 — folded into delivery_platforms | — | — | — |
fiscal_efatura | integration | annual | — | integration.fiscal += efatura | archived v3.6.7 — folded into license_annual | — |
fiscal_hugin | integration | annual | ₺2,990 (299000) | integration.fiscal += hugin | ✅ | — |
caller_id_integration | integration | annual | ₺1,490 (149000) | integration.caller += generic | ✅ | — |
sms_integration | integration | annual | ₺990 (99000) | integration.sms += * | ✅ | — |
extra_branch | capacity | annual | ₺3,990 (399000) | limit.maxBranches +1, feature.multiLocation | ✅ | — |
credit_ai_photo_100 | credit | one-time | ₺690 (69000) | — (100 PHOTO credits) | — | module_ai_studio |
credit_ai_video_20 | credit | one-time | ₺890 (89000) | — (20 VIDEO credits) | — | module_ai_studio |
credit_ai_3d_10 | credit | one-time | ₺790 (79000) | — (10 MODEL3D credits) | — | module_ai_studio |
credit_sms_500 | credit | one-time | ₺490 (49000) | — (500 SMS credits) | — | sms_integration |
onsite_install_full | service | one-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:
code | Meaning |
|---|---|
LICENSE_REQUIRED | The product needs a licence and the cart has none either |
ADDON_ALREADY_OWNED | This product (or the licence) is already active |
ADDON_ALREADY_GRANTED | Everything the product grants is already in the entitlement set |
ADDON_REQUIRES_DEPENDENCY | Its dependency is neither owned nor in the cart |
ADDON_LIMIT_REDUNDANT | The capacity it adds is already unlimited |
ADDON_MAX_QUANTITY | The 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_500 → sms_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", "...": "..." }
}
]| Field | Meaning |
|---|---|
status | active · past_due (grace window, entitlement still live) · cancelled · expired |
quantity / pendingQuantity | Current units / units to apply at the next anniversary (capacity downgrades are renewal-time) |
chargedCents | What was actually charged (prorated) for this period; 0 for comps |
origin | purchase · 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.
Related
- Entitlement Matrix — the free core, product
grants, licence suppression, and the
ENTITLEMENT_REQUIREDenvelope. - 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_accessmodule grantsfeature.apiAccess). - Operator experience (browsing, buying, and tracking products in the panel): Marketplace help.