Skip to content

Product Management

This guide is the canonical reference for the admin product write surface. Every property surfaced on the public client product list / detail endpoints is settable here in a single create or update call.

The same DTO is used by POST /admin/products and (with all fields optional) by PUT /admin/products/:id. Schemas are auto-generated into the admin OpenAPI spec — see CreateProductDto and UpdateProductDto.


Anatomy of a product

products ← name, slug, description, brand_id, product_type_id, …
├── product_variants ← SKUs, prices, currency, inventory controls
│ └── partner_product_sources ← (variant × partner) priority-ordered sources
├── product_categories
├── product_tags ← also drives `is_new` / `is_popular` (slugs `new` / `popular`)
├── product_payment_methods
├── instruction_steps ← JSON array of redemption steps
└── fulfillment_config ← typed JSON describing recipient inputs + delivery

Everything from instruction_image and instruction_video to per-variant stock_cap and per-partner send_value is settable on a single product write — there is no second-pass requirement.


Top-level product fields

FieldTypeNotes
name, slugstringslug must be unique across all products.
short_description, descriptionstringPublic copy.
status'draft' | 'active' | 'archived' | 'pending' | 'discarded'Only active is shown to customers.
brand_idintFK to brands. Brand must be active to surface on client API.
product_type_idintFK to product_types.
redirect_urlstring | nullUsed when fulfillment_config.modality = 'REDIRECT'.
thumbnail, bannerstringURLs to images already uploaded via the file manager. Inherited from the brand on first create when omitted.
instruction_image, instruction_videostringAsset URLs.
instruction_stepsarrayEither string steps or rich { step, title, description } objects. Stored as JSON.
delivery_methodstringLegacy — prefer fulfillment_config.delivery_type. Still returned in the public response for back-compat.
validity_text, refund_policystringPublic copy.
meta_title, meta_description, seo_og_image_urlstringSEO.
featured, trendingboolToggles for homepage sections of the same names.
support_configobjectFree-form support metadata (help-desk URL, agent contact, …).
fulfillment_configobject (typed)See Fulfillment config below.
category_idsint[]FK list — replaces existing categories on update.
tag_idsint[]FK list. Attach the new / popular tag slugs to surface in homepage sections.
payment_method_idsint[]FK list (preferred). Falls back to payment_methods: string[] (slugs/codes).
variantsCreateProductVariantDto[]Optional inline variants on create. See Variants.

Variants

Each variant is one purchasable SKU on a product. Inline-create on the same request as the product, or attach later via POST /admin/products/:id/variants.

{
"name": "Standard 1-month",
"sku": "SKU-1M",
"currency": "BDT",
"original_price": 1200,
"price": 1100,
"discount_type": "percentage",
"discount_value": 0,
"validity_days": 30,
"is_active": true,
// Inventory controls — see docs/INVENTORY.md
"track_inventory": true,
"stock_cap": 1000,
"low_stock_threshold": 5,
// Optional: assign a partner source in the same call (writes a
// partner_product_sources row). Omit to keep the variant inhouse.
"partner_id": 5,
"partner_cost": 1080,
"partner_priority": 1,
"partner_external_offer_id": "off-basic-1m",
"partner_sku": "OFFER-1M",
"partner_currency": "USD",
"partner_offer_type": "VOUCHER",
"partner_price_type": "FIXED",
"partner_send_value": 25,
"partner_send_currency": "USD",
"partner_metadata": { "region": "US" },
"partner_source_active": true
}

Inventory controls

FieldDefaultEffect
track_inventorytrueWhen true, the order pipeline reserves and decrements stock. Set to false for variants with infinite supply (dynamic partner-issued vouchers).
stock_capnullOptional hard ceiling on on-hand units. Independent of preloaded voucher count.
low_stock_threshold5When stock drops to or below this number, a low_stock admin_alerts row is raised.

Multi-partner: priority-ordered sources

Each variant can have many partners attached. The fulfillment engine picks the lowest priority number with is_active = true (ties broken by lowest partner_cost). Manage many sources at once via the dedicated endpoints:

MethodEndpointPurpose
GET/admin/partner-sources?variant_id=42List sources for a variant.
POST/admin/partner-sourcesAttach a new partner to a variant.
PUT/admin/partner-sources/:idUpdate one source (cost, priority, offer details, metadata).
DELETE/admin/partner-sources/:idDetach a partner.
PATCH/admin/partner-sources/prioritiesBulk-reorder by [{ id, priority }].

metadata_replace flag

PUT /admin/partner-sources/:id defaults to merging incoming metadata on top of existing keys. Send metadata_replace: true to do a hard overwrite (matches the variant DTO partner_metadata semantics, which are merge-only).


Fulfillment config

fulfillment_config is the typed JSON describing how vouchers are delivered and what recipient inputs the customer must provide.

{
"modality": "CODE", // 'CODE' | 'REDIRECT' | 'TOPUP' | 'ESIM'
"delivery_type": "instant",
"average_delivery_seconds": 30,
// Per-field configuration for the storefront's checkout form. Each entry
// declares name, label, type and required. The single-identifier rule
// (email | player_id | account_id | uid) applies — at most one identifier
// key may appear across all entries.
"fields": [
{ "name": "player_uid", "label": "Player UID", "type": "text", "required": true },
{ "name": "zone_id", "label": "Zone ID", "type": "text", "required": false }
]
}

The public API at /api/v1/products/:id returns the same shape under fulfillment_input:

"fulfillment_input": {
"mode": "CODE",
"fields": [
{ "name": "player_uid", "label": "Player UID", "type": "text", "required": true },
{ "name": "zone_id", "label": "Zone ID", "type": "text", "required": false }
],
"identifier_field": null
}

Supported type values map to standard HTML inputs: text (default), email, number, phone, select, url. Unknown types are passed through verbatim so storefronts can implement custom renderers.

Single-identifier rule

A product accepts exactly one recipient identifier:

KeyUse
emailVoucher emailed to a recipient.
player_idGame account / player ID (e.g. Free Fire UID).
account_idAccount number for direct top-ups.
uidGeneric user ID.

If you list more than one identifier key in fields[].name, the admin write API rejects with 400 before persistence:

fields[].name may contain at most one of: email, player_id, account_id, uid

The client-side read path additionally normalises any legacy storage rows so the storefront only ever renders one identifier input — it picks the highest-priority key by the order above.

Order-time enforcement

When the customer places an order, POST /api/v1/orders validates the submitted fulfillment_input per item against the resolved identifier and rejects multi-identifier or mismatched payloads with a 400. See the checkout flow guide.


Payment methods

Two equivalent ways to set the allowed gateways for a product:

// Preferred — by FK ID from the `payment_methods` lookup table:
{ "payment_method_ids": [1, 3, 4] }
// Or by slug / code:
{ "payment_methods": ["bkash", "nagad", "sslcommerz"] }

When both are provided, payment_method_ids wins.

Inactive payment methods (status = false) are filtered out of the public client product response automatically — no admin action needed.


Categories & tags

FieldBehavior
category_idsReplaces the product’s product_categories rows.
tag_idsReplaces the product’s product_tags rows. Attach the canonical new / popular tags (slugs) to surface in homepage sections.

Sorting & curation

Both products and variants carry a sort_order INTEGER DEFAULT 0 column. Lower values surface first; ties break by id ASC for deterministic pagination.

Reorder them in a single atomic call:

PATCH /admin/products/reorder # cross-catalogue product order
PATCH /admin/products/:productId/variants/reorder # variant order inside one product
{
"items": [
{ "id": 7, "sort_order": 0 },
{ "id": 2, "sort_order": 1 },
{ "id": 11, "sort_order": 2 }
]
}

The client API picks up the new order on the next request. The full reorder contract (limits, error codes, every endpoint in the family) lives in the Sorting & curation guide.


Examples

POST /admin/products
Authorization: Bearer $ADMIN_TOKEN
Content-Type: application/json
{
"name": "Free Fire 100 Diamonds",
"slug": "free-fire-100-diamonds",
"status": "draft",
"brand_id": 12,
"product_type_id": 3,
"fulfillment_config": {
"modality": "TOPUP",
"fields": [
{ "name": "player_id", "label": "Free Fire UID", "type": "text", "required": true }
]
}
}

  • Checkout flow — how products surface to the client and how fulfillment_input is validated at order time.
  • Admin fulfillment recovery — manual codes, partner retry, attempt log.
  • Inventorytrack_inventory, stock_cap, low_stock_threshold semantics in detail.