Brand Management Guide
Overview
Brands represent the manufacturers or owners of products in your catalog. Every product in the Operations module can be associated with a brand, giving you a structured way to track provenance, present brand information to customers, and report on brand-level performance across your inventory.
The brand entity is intentionally lightweight: it captures identity fields (name, slug, logo, website), a description for internal or customer-facing use, and a simple active/inactive status flag. This simplicity makes brands fast to create and maintain. Bulk operations and export capabilities allow operations teams to manage large brand catalogs efficiently without requiring individual record edits.
Brand Lifecycle
Brands follow a two-state active/inactive lifecycle. Unlike entities with complex status progressions, a brand is either available for product association or it is not. This keeps catalog hygiene straightforward: deactivating a brand hides it from product creation workflows without deleting historical data or orphaning existing product records.
| Status | Description | Effect on Products |
|---|---|---|
| Active | Brand is available for use | Products can be created or updated to use this brand |
| Inactive | Brand is suspended from use | Existing product associations are preserved; new associations are blocked |
Deactivation vs. Deletion
Deactivating a brand is non-destructive. Products already linked to the brand retain the association and remain fully functional. Use deactivation when a brand is temporarily unavailable or under review. Use deletion only when the brand was created in error and has no product associations.
Brand Fields
| Field | Type | Required | Description |
|---|---|---|---|
id | ULID string | Auto-generated | Unique identifier, e.g. 01hwxyz123abc456def789ghi0 |
name | string | Yes | Display name of the brand |
slug | string | No | URL-safe identifier, auto-generated from name if omitted |
description | string | No | Internal or customer-facing brand description |
logo_url | string | No | Absolute URL to the brand's logo image |
website_url | string | No | Brand's public website URL |
is_active | boolean | No | Active status flag, defaults to true |
created_at | ISO 8601 datetime | Auto-generated | Record creation timestamp |
updated_at | ISO 8601 datetime | Auto-generated | Record last-modified timestamp |
API Endpoints
All brand endpoints are scoped under /api/v1/operations/brands. Every request requires a valid Bearer token.
List Brands
Endpoint: GET /api/v1/operations/brands
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
search | string | Filter by name or slug (partial match) |
is_active | boolean | Filter by active status (true or false) |
has_products | boolean | Filter to brands with (true) or without (false) associated products |
sort_by | string | Sort field: name, created_at, updated_at |
sort_direction | string | asc or desc (default: asc) |
page | integer | Page number (default: 1) |
per_page | integer | Results per page (default: 25, max: 100) |
Example Request:
GET /api/v1/operations/brands?is_active=true&has_products=true&sort_by=name&sort_direction=asc&per_page=50Response (200 OK):
{
"success": true,
"message": "Brands retrieved successfully",
"data": [
{
"id": "01hwxyz123abc456def789ghi0",
"name": "Acme Industrial",
"slug": "acme-industrial",
"description": "Leading manufacturer of industrial tools and equipment.",
"logo_url": "https://cdn.example.com/brands/acme-industrial-logo.png",
"website_url": "https://www.acme-industrial.com",
"is_active": true,
"created_at": "2025-10-01T08:00:00Z",
"updated_at": "2025-11-15T14:30:00Z"
},
{
"id": "01hwxyz789def012ghi345jkl6",
"name": "BlueStar Electronics",
"slug": "bluestar-electronics",
"description": "Consumer electronics and accessories.",
"logo_url": "https://cdn.example.com/brands/bluestar-logo.png",
"website_url": "https://www.bluestar-elec.com",
"is_active": true,
"created_at": "2025-10-12T11:20:00Z",
"updated_at": "2025-10-12T11:20:00Z"
}
],
"meta": {
"current_page": 1,
"per_page": 50,
"total": 38,
"last_page": 1
}
}Create a Brand
Endpoint: POST /api/v1/operations/brands
Request Body:
{
"name": "Acme Industrial",
"slug": "acme-industrial",
"description": "Leading manufacturer of industrial tools and equipment.",
"logo_url": "https://cdn.example.com/brands/acme-industrial-logo.png",
"website_url": "https://www.acme-industrial.com",
"is_active": true
}Required Fields:
name- Display name of the brand
Optional Fields:
slug- Auto-generated fromnameif omitted; must be unique within the tenantdescription- Free-text descriptionlogo_url- Must be a valid URL if providedwebsite_url- Must be a valid URL if providedis_active- Defaults totrue
Response (201 Created):
{
"success": true,
"message": "Brand created successfully",
"data": {
"id": "01hwxyz123abc456def789ghi0",
"name": "Acme Industrial",
"slug": "acme-industrial",
"description": "Leading manufacturer of industrial tools and equipment.",
"logo_url": "https://cdn.example.com/brands/acme-industrial-logo.png",
"website_url": "https://www.acme-industrial.com",
"is_active": true,
"created_at": "2025-12-17T10:30:00Z",
"updated_at": "2025-12-17T10:30:00Z"
},
"meta": {}
}Error Response (422 Unprocessable Entity):
{
"success": false,
"message": "The given data was invalid.",
"data": null,
"meta": {
"errors": {
"name": ["The name field is required."],
"slug": ["The slug has already been taken."]
}
}
}Get Brand Details
Endpoint: GET /api/v1/operations/brands/{id}
Example:
GET /api/v1/operations/brands/01hwxyz123abc456def789ghi0Response (200 OK):
{
"success": true,
"message": "Brand retrieved successfully",
"data": {
"id": "01hwxyz123abc456def789ghi0",
"name": "Acme Industrial",
"slug": "acme-industrial",
"description": "Leading manufacturer of industrial tools and equipment.",
"logo_url": "https://cdn.example.com/brands/acme-industrial-logo.png",
"website_url": "https://www.acme-industrial.com",
"is_active": true,
"created_at": "2025-10-01T08:00:00Z",
"updated_at": "2025-11-15T14:30:00Z"
},
"meta": {}
}Error Response (404 Not Found):
{
"success": false,
"message": "Brand not found.",
"data": null,
"meta": {}
}Update a Brand
Endpoint: PUT /api/v1/operations/brands/{id}
All fields are optional - send only the fields you want to change.
Request Body:
{
"description": "Updated: premium industrial tools for professional contractors.",
"logo_url": "https://cdn.example.com/brands/acme-industrial-logo-v2.png",
"website_url": "https://www.acme-industrial.com/professional",
"is_active": true
}Response (200 OK):
{
"success": true,
"message": "Brand updated successfully",
"data": {
"id": "01hwxyz123abc456def789ghi0",
"name": "Acme Industrial",
"slug": "acme-industrial",
"description": "Updated: premium industrial tools for professional contractors.",
"logo_url": "https://cdn.example.com/brands/acme-industrial-logo-v2.png",
"website_url": "https://www.acme-industrial.com/professional",
"is_active": true,
"updated_at": "2025-12-17T14:00:00Z"
},
"meta": {}
}Delete a Brand
Endpoint: DELETE /api/v1/operations/brands/{id}
Before Deleting
Deletion is permanent and will fail if the brand has associated products. Either reassign or delete the products first, or consider deactivating the brand instead to preserve historical data.
Response (200 OK):
{
"success": true,
"message": "Brand deleted successfully",
"data": null,
"meta": {}
}Error Response (409 Conflict):
{
"success": false,
"message": "Cannot delete brand with associated products. Reassign products before deleting.",
"data": null,
"meta": {}
}Bulk Operations
Bulk Activate
Endpoint: POST /api/v1/operations/brands/bulk-activate
Request Body:
{
"brand_ids": [
"01hwxyz123abc456def789ghi0",
"01hwxyz789def012ghi345jkl6",
"01hwxyzabc123def456ghi789j0"
]
}Response (200 OK):
{
"success": true,
"message": "Brands activated successfully",
"data": {
"processed": 3,
"successful": 3,
"failed": 0
},
"meta": {}
}Bulk Deactivate
Endpoint: POST /api/v1/operations/brands/bulk-deactivate
Request Body:
{
"brand_ids": [
"01hwxyz123abc456def789ghi0",
"01hwxyz789def012ghi345jkl6"
]
}Response (200 OK):
{
"success": true,
"message": "Brands deactivated successfully",
"data": {
"processed": 2,
"successful": 2,
"failed": 0
},
"meta": {}
}Bulk Delete
Endpoint: POST /api/v1/operations/brands/bulk-delete
Irreversible Operation
Bulk delete is permanent. Brands that still have associated products will be skipped and reported in the failed count. Review brand-product associations before issuing a bulk delete.
Request Body:
{
"brand_ids": [
"01hwxyz123abc456def789ghi0",
"01hwxyz789def012ghi345jkl6"
]
}Response (200 OK):
{
"success": true,
"message": "Bulk delete completed",
"data": {
"processed": 2,
"successful": 1,
"failed": 1,
"failures": [
{
"id": "01hwxyz789def012ghi345jkl6",
"reason": "Brand has associated products"
}
]
},
"meta": {}
}Brand Statistics
Endpoint: GET /api/v1/operations/brands/statistics
Returns aggregate counts for the brand catalog. Use this endpoint for dashboard widgets or catalog health checks.
Response (200 OK):
{
"success": true,
"message": "Brand statistics retrieved successfully",
"data": {
"total": 45,
"active": 38,
"inactive": 7,
"with_products": 32,
"without_products": 13
},
"meta": {}
}Export Brands
Endpoint: GET /api/v1/operations/brands/export
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
format | string | Export format: csv or json |
Example Requests:
GET /api/v1/operations/brands/export?format=csv
GET /api/v1/operations/brands/export?format=jsonThe response is the exported file content with an appropriate Content-Disposition header for browser download. The export includes all brand fields and respects the current tenant's data boundary.
Business Scenarios
Scenario 1: Onboarding a New Supplier's Brand Portfolio
Context: A new supplier agreement covers eight brands. You need to create all of them, set logos from the supplier's media kit, and immediately make them available for the product import that follows.
Workflow:
- Create each brand with name, logo URL, website, and description sourced from the supplier's brand guide
- Confirm all are active (default behavior) so they're immediately selectable during product import
- Run the statistics endpoint to verify the expected count of active brands
API Calls:
# 1. Create brand one
POST /api/v1/operations/brands
{
"name": "Acme Industrial",
"description": "Industrial tools for professional contractors.",
"logo_url": "https://cdn.example.com/brands/acme-industrial-logo.png",
"website_url": "https://www.acme-industrial.com"
}
# 2. Repeat for remaining brands...
# 3. Verify with statistics
GET /api/v1/operations/brands/statistics
# Confirm "active" count increased by 8Scenario 2: Seasonal Brand Suspension and Reactivation
Context: Three seasonal brands are no longer being stocked for the off-season. You want to prevent them from appearing in product creation screens without deleting any data. They will be reactivated when stock returns.
Workflow:
- Identify the IDs of the seasonal brands
- Bulk deactivate them before the off-season begins
- Verify using the list endpoint filtered to inactive brands
- At the start of the next season, bulk activate the same brands
API Calls:
# 1. Deactivate for off-season
POST /api/v1/operations/brands/bulk-deactivate
{
"brand_ids": [
"01hwxyz123abc456def789ghi0",
"01hwxyz789def012ghi345jkl6",
"01hwxyzabc123def456ghi789j0"
]
}
# 2. Verify
GET /api/v1/operations/brands?is_active=false
# Confirm the three brands appear
# 3. Reactivate at season start
POST /api/v1/operations/brands/bulk-activate
{
"brand_ids": [
"01hwxyz123abc456def789ghi0",
"01hwxyz789def012ghi345jkl6",
"01hwxyzabc123def456ghi789j0"
]
}Scenario 3: Catalog Audit and Cleanup
Context: A periodic catalog review reveals several brands that were created as placeholders and never had products assigned. These should be removed to keep the catalog clean.
Workflow:
- Pull brand statistics to quantify the scope (
without_productscount) - List all brands filtered to
has_products=false - Review the list and identify which are safe to delete vs. which are simply pending product import
- Export the final list for record-keeping before deletion
- Bulk delete the confirmed placeholder brands
API Calls:
# 1. Check scope
GET /api/v1/operations/brands/statistics
# Note "without_products" count
# 2. Retrieve the list
GET /api/v1/operations/brands?has_products=false&per_page=100
# 3. Export for audit trail
GET /api/v1/operations/brands/export?format=csv
# 4. Delete confirmed placeholders
POST /api/v1/operations/brands/bulk-delete
{
"brand_ids": [
"01hwxyzaaa111bbb222ccc333d0",
"01hwxyzeee555fff666ggg777h0"
]
}Best Practices
1. Slug Consistency
Allow the system to auto-generate slugs from the brand name unless you have a specific reason to set them manually. Auto-generated slugs are consistent, URL-safe, and unique-enforced by the system. If you override a slug, use lowercase letters, numbers, and hyphens only - no spaces or special characters. Slugs cannot be changed after products reference the brand in external integrations that use slug-based lookups.
2. Logo and Asset Management
Host brand logos on a stable CDN or object storage service before creating or updating brand records. Avoid linking to third-party URLs that may change or expire, as broken logo URLs degrade the product catalog UI. Use a consistent naming convention for asset files (e.g., {slug}-logo.png) to make bulk updates straightforward. Recommended minimum logo dimensions are 400×400 pixels in PNG format with a transparent background.
3. Maintain Active/Inactive Discipline
Reserve the is_active=false state for brands that are genuinely suspended from use. Avoid leaving brands inactive indefinitely as a form of soft-archiving - if a brand is permanently retired and has no products, delete it. If it has products, deactivate it and document the reason in an internal note or your team's wiki. Running GET /api/v1/operations/brands/statistics monthly gives you a quick signal when the inactive count is growing without purpose.
4. Pre-Import Verification
Before bulk-importing products that reference brands by name or slug, verify that all expected brands exist and are active by calling GET /api/v1/operations/brands?is_active=true&per_page=100. Compare the returned list against your import manifest. Creating missing brands before the product import avoids partial import failures and eliminates the need for post-import cleanup.
5. Treat Statistics as a Health Signal
The with_products vs. without_products split in the statistics endpoint is a direct measure of catalog health. A large without_products count typically means either brands were pre-created speculatively (clean them up) or a product import failed partway (investigate). Build a periodic check of this endpoint into your catalog maintenance routine.
Integration Points
With Products
Brands are a core attribute of the Product entity. When creating or updating a product, you reference the brand by its ULID. Deactivating a brand does not cascade to products - existing associations are preserved. If you delete a brand, all products currently associated with it must be reassigned first. Refer to the Product Management Guide for details on the brand field in product create/update requests.
With Product Variants
Product variants inherit their brand association from the parent product. You do not set a brand at the variant level. When filtering inventory or reports by brand, all variants of a branded product are implicitly included.
With Reporting and Analytics
The statistics endpoint is designed for dashboard consumption. For deeper brand-level reporting - such as revenue or stock value per brand - use the Analytics module's product-level aggregations filtered by brand ID.
With Exports and Data Pipelines
The export endpoint produces a flat representation of brand data suitable for loading into spreadsheet tools, BI platforms, or syncing back to a supplier portal. If you need scheduled exports, integrate the export endpoint into a cron-driven data pipeline using your infrastructure's scheduled job tooling.
Troubleshooting
Brand Creation Fails with Slug Conflict
Error: "The slug has already been taken."
Cause: Another brand in the same tenant already uses the auto-generated or manually supplied slug.
Solution:
- Search for the conflicting brand:
GET /api/v1/operations/brands?search={slug} - If the existing brand is the same entity (e.g., a duplicate entry), delete or update the duplicate
- If the brands are legitimately distinct, supply a unique slug manually (e.g.,
acme-industrial-toolsvs.acme-industrial-supplies)
Cannot Delete Brand
Error: "Cannot delete brand with associated products."
Cause: One or more products are still linked to this brand.
Solution:
- Retrieve the brand's products:
GET /api/v1/operations/products?brand_id={brand_id} - Reassign each product to a different brand via
PUT /api/v1/operations/products/{id}with an updatedbrand_id - Alternatively, if the products should also be removed, delete the products first
- Retry the brand deletion once no products remain associated
Bulk Operation Reports Unexpected Failures
Symptom: A bulk activate/deactivate/delete call returns a failed count greater than zero without clear explanation.
Solution:
- Check the
failuresarray in the response body - each entry includes the brand ID and the failure reason - Common causes: brand ID not found (ULID typo or wrong tenant context), attempting to delete a brand with products
- Fix the listed issues individually, then re-run the bulk operation with only the previously-failed IDs
Related Documentation
- Product Management Guide - Associating brands with products
- Product Variants Guide - How variant inheritance works with brands
- Operations Module Overview - Full list of Operations module capabilities