# Products

Create products in the dashboard or via API — they live directly on your Southbill account.

# Products

A **Product** describes something you sell — a shoe, a subscription tier, a service. Products in Southbill are created **directly on your Southbill account** — either via the dashboard (**Dashboard → Products → New**) or via the public API described below.

## Fields

| Field | Description |
|---|---|
| `name` | Public name shown on receipts, invoices and checkout. |
| `description` | Long description. |
| `images[]` | Up to 8 image URLs. First image is used as the thumbnail. |
| `tax_code` | Optional Southbill tax code (`txcd_…`). |
| `unit_label` | e.g. `"seat"`, `"month"`. |
| `statement_descriptor` | What appears on the buyer's card statement (≤ 22 chars). |
| `url` | Link back to your product page. |
| `shippable` | Physical goods = `true`. |
| `package_dimensions` | Length / width / height (cm) and weight (g). |
| `metadata` | Free-form key/value. |
| `tags[]` | **southbill-internal** — used for catalog auto-grouping. |
| `default_price` | The **Price** object linked as the default. |

## Minimum price

Every `unit_amount` must be **≥ 250 minor units (2.50)** in the price's currency. Smaller amounts don't cover PSP + Southbill fees.

---

## API — Create a product

`POST https://api.southbill.com/v1/products`

Authenticate with a **secret API key** (`sk_live_…` / `sk_test_…`) — scope `products:write`. Pass an optional `Idempotency-Key` header to make retries safe.

```bash
curl https://api.southbill.com/v1/products \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: prod-launch-sneaker-v1" \
  -d '{
    "name": "Runner 01",
    "description": "Lightweight everyday shoe",
    "images": ["https://cdn.example.com/runner-01.jpg"],
    "shippable": true,
    "metadata": { "sku": "RUN-01" },
    "tags": ["shoes", "new"],
    "price": {
      "currency": "eur",
      "unit_amount": 8900
    }
  }'
```

Pass a `price` object to create the Product **and** its default Price in one call. Omit it if you want to add prices later.

### One-time vs recurring prices

A Product itself is just the catalog item. Whether it is charged once or repeatedly is controlled by the attached **Price**:

- **One-time paid product**: send `price.currency` + `price.unit_amount` only.
- **Recurring paid product**: send the same price fields plus `price.recurring`.

Recurring price fields:

| Field | Description |
|---|---|
| `price.recurring.interval` | Required for recurring prices. Allowed: `day`, `week`, `month`, `year`. |
| `price.recurring.interval_count` | Optional. Defaults to `1`. Example: `3` + `month` = every 3 months. |
| `price.recurring.trial_period_days` | Optional trial length in days. |
| `price.recurring.usage_type` | Optional: `licensed` or `metered`. |

Example — monthly recurring product:

```bash
curl https://api.southbill.com/v1/products \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: prod-pro-plan-monthly-v1" \
  -d '{
    "name": "Pro Plan",
    "description": "Monthly access to Pro features",
    "metadata": { "sku": "PRO-MONTHLY" },
    "tags": ["subscription", "pro"],
    "price": {
      "currency": "eur",
      "unit_amount": 2900,
      "recurring": {
        "interval": "month",
        "interval_count": 1,
        "trial_period_days": 14
      }
    }
  }'
```

The response will include a recurring Price with `type: "recurring"` and the recurring interval fields.

### Response

```json
{
  "id": "prod_ABC123",
  "object": "product",
  "name": "Runner 01",
  "active": true,
  "default_price": "price_XYZ789",
  "prices": [{ "id": "price_XYZ789", "currency": "eur", "unit_amount": 8900, ... }],
  "metadata": { "sku": "RUN-01" },
  "tags": ["shoes", "new"]
}
```

The returned `prod_…` and `price_…` IDs are what you use everywhere else — catalog management and invoice product selection. Recurring prices define the billing terms, but automatic subscription billing requires a recurring-billing flow.

---

## API — Other product operations

| Method | Path | Description |
|---|---|---|
| `GET` | `/v1/products` | List products (params: `limit`, `starting_after`). Scope `products:read`. |
| `GET` | `/v1/products/{id}` | Retrieve a product with all its prices. |
| `POST` | `/v1/products/{id}` | Update fields (`name`, `description`, `images`, `active`, `metadata`, `tags`, …). |
| `DELETE` | `/v1/products/{id}` | Archive the product and all its prices (`active=false`). |
| `POST` | `/v1/products/{id}/default_price` | Body `{ "price": "price_…" }` — change the default price. |

---

## API — Prices on a product

| Method | Path | Description |
|---|---|---|
| `POST` | `/v1/products/{id}/prices` | Create a new Price on this product. Fields: `currency`, `unit_amount`, `nickname`, `recurring`, `tax_behavior`, `lookup_key`, `set_as_default`, `metadata`. |
| `GET` | `/v1/products/{id}/prices` | List all prices on this product. |

Prices are **immutable** except for `active`, `nickname`, `tax_behavior`, `lookup_key`, `metadata`. To change amount or currency, create a new Price and mark the old one inactive.

```bash
curl https://api.southbill.com/v1/products/prod_ABC123/prices \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "currency": "eur",
    "unit_amount": 9900,
    "nickname": "2026 launch price",
    "set_as_default": true
  }'
```

---

## Using a product

- **In an invoice**: New invoice → **Add from products** → the item is locked to the product's Southbill values.
- **In a Checkout Session**: pass `line_items[].price_id: "price_…"` so receipts and analytics link back to the catalog entry.

## Product ID vs Price ID

| ID | What it is | When you use it |
|---|---|---|
| `prod_…` | The **container** — name, description, images. | Reporting, catalog management. |
| `price_…` | The **billable price** — amount, currency, one-time or recurring. | **Every transaction and invoice line-item**. |

Rule of thumb: to charge someone you always need a **price** (or an ad-hoc `amount`), never just a product.


## Idempotency

All `POST` endpoints (`/v1/products`, `/v1/products/{id}`, `/v1/products/{id}/prices`, `/v1/products/{id}/default_price`) accept `Idempotency-Key`. Same key + same body replays the original response; same key + different body returns **`409 Conflict`** with `type: "idempotency_error"`, `code: "idempotency_key_reused"`. See [Error reference](/docs/api/errors).

