Products
A product is the catalog entry buyers see. Each product has one or more variants (size, color, SKU-level differences) and every stock row + shipment line + license pin to a variant, not the product. The Python SDK exposes both layers behind fulkruma.products. For HTTP shapes and the full field reference, see API → Products.
Namespace
fulkruma.products # ProductsResources
Every method below shares the same httpx.Client as the parent.
Methods
create
fulkruma.products.create(body: dict, *, on_behalf_of: str | None = None) -> dict
Creates a product. Only name is required. type defaults to "physical"; pass "digital" for download-only goods (which then unlock the deliveries + licenses surfaces) or "license" for software sold as a key. The SDK auto-mints an idempotency key, so retries are safe.
result = fulkruma.products.create({
"name": "Pawpado Premium License",
"type": "digital",
"description": "GPU portal — 12-month subscription",
"licenseEnabled": True,
"maxActivations": 3,
"sku": "pawpado-premium-12m",
})
print(result["product"]["id"]) # "prod_01HX..."
sku is your handle into your own catalog. Products that Storlaunch mirrors into Fulkruma also carry externalRef + externalSource ("storlaunch"); those are set by that sync only — create and update don't accept them.
get
fulkruma.products.get(product_id: str, *, on_behalf_of: str | None = None) -> dict
Fetches one product + every variant in one response. Raises FulkrumaError(404, "not_found", ...) for missing or cross-workspace IDs.
result = fulkruma.products.get("prod_01HX...")
print(len(result["product"]["variants"]), "variants")
list
fulkruma.products.list(
*,
archived: bool | None = None,
on_behalf_of: str | None = None,
) -> dict
Returns every product in the workspace. Pass archived=True to include archived ones, archived=False (or omit) to exclude. Not paginated.
result = fulkruma.products.list(archived=False)
for p in result["products"]:
print(p["name"], len(p["variants"]), "variants")
update
fulkruma.products.update(
product_id: str,
patch: dict,
*,
on_behalf_of: str | None = None,
) -> dict
Partial update. To toggle license-mode on an existing product, pass {"licenseEnabled": True}; the next licenses.issue call will succeed instead of returning 400.
fulkruma.products.update("prod_01HX...", {
"description": "Updated copy",
"licenseEnabled": True,
})
archive
fulkruma.products.archive(product_id: str, *, on_behalf_of: str | None = None) -> dict
Soft-delete. Variants archive alongside the parent automatically. Historic shipments/licenses/stock movements still reference the archived product so audit trails keep working.
fulkruma.products.archive("prod_01HX...")
add_variant
fulkruma.products.add_variant(
product_id: str,
body: dict,
*,
on_behalf_of: str | None = None,
) -> dict
Adds a variant. name is required (e.g. "Size L / Red"); SKU, prices, low-stock threshold, weight are optional.
result = fulkruma.products.add_variant("prod_01HX...", {
"name": "Size L",
"sku": "TSHIRT-L",
"priceCents": 2_500_000,
"costCents": 1_250_000,
"lowStockThreshold": 5,
"isDefault": True,
})
print(result["variant"]["id"])
Variants share the parent product's type — you can't mix physical + digital variants under one product.
update_variant / archive_variant
fulkruma.products.update_variant(
product_id: str, variant_id: str, patch: dict,
*, on_behalf_of: str | None = None,
) -> dict
fulkruma.products.archive_variant(
product_id: str, variant_id: str,
*, on_behalf_of: str | None = None,
) -> dict
fulkruma.products.update_variant("prod_01HX...", "var_01HX...", {
"priceCents": 3_000_000,
})
fulkruma.products.archive_variant("prod_01HX...", "var_01HX...")
Archiving the last live variant on a product is allowed but unusual — you typically archive the whole product instead.
Types
Every method returns a plain dict matching the API envelope. The product shape:
{
"product": {
"id": "prod_...",
"accountId": "acc_...",
"name": "...",
"sku": "..." | None,
"description": "..." | None,
"type": "physical" | "digital" | "license",
"weight": float | None,
"licenseEnabled": bool,
"maxActivations": int | None,
"externalRef": "..." | None,
"externalSource": "..." | None,
"variants": [{...}, ...],
"archivedAt": "..." | None,
"createdAt": "...",
"updatedAt": "..."
}
}
See API → Products for per-field validation rules.
Common patterns
Create a simple product with one default variant. Most products have one SKU:
def create_simple(fulkruma, name: str, sku: str, price_cents: int) -> str:
pres = fulkruma.products.create({"name": name})
product_id = pres["product"]["id"]
fulkruma.products.add_variant(product_id, {
"name": "Default",
"sku": sku,
"priceCents": price_cents,
"isDefault": True,
})
return product_id
Sync from your own catalog. If your own system owns the source-of-truth catalog, use sku to dedupe:
def upsert_product(fulkruma, sku: str, name: str, price_cents: int) -> str:
result = fulkruma.products.list()
existing = next((p for p in result["products"] if p.get("sku") == sku), None)
if existing:
fulkruma.products.update(existing["id"], {"name": name})
return existing["id"]
pres = fulkruma.products.create({"name": name, "sku": sku})
pid = pres["product"]["id"]
fulkruma.products.add_variant(pid, {
"name": "Default", "priceCents": price_cents, "isDefault": True,
})
return pid
Errors
err.status |
err.code |
Cause |
|---|---|---|
400 |
VALIDATION |
Missing name, bad type, a negative dimension or price. |
403 |
NO_ACCOUNT |
The credentials resolve to no workspace. |
404 |
NOT_FOUND |
Product or variant ID missing or in another workspace. |
Next
- Stock — per-variant per-warehouse inventory.
- Licenses — digital-product license keys.
- API → Products — HTTP-level reference.