> ## Documentation Index
> Fetch the complete documentation index at: https://docs.waffo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Create Product

> Create a new one-time purchase product with multi-currency pricing

Create a new one-time purchase product with multi-currency pricing.

```
POST /v1/actions/onetime-product/create-product
```

**Authentication:** API Key

## Request Body

| Field         | Type           | Required | Description                                                                                                       |
| ------------- | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `storeId`     | string         | Yes      | Store ID (Short ID format `STO_xxx`)                                                                              |
| `name`        | string         | Yes      | Product name (max 64 chars)                                                                                       |
| `description` | string \| null | No       | Product description (supports Markdown; pass `null` or `""` to clear)                                             |
| `prices`      | object         | Yes      | Multi-currency pricing map (see below)                                                                            |
| `media`       | array          | No       | Product images/videos (see below)                                                                                 |
| `successUrl`  | string \| null | No       | Redirect URL after successful purchase (max 512 chars, must be a valid http(s) URL; pass `null` or `""` to clear) |
| `metadata`    | object         | No       | Custom key-value data (max 50 keys)                                                                               |

## Price Object Format

Prices are a map of ISO 4217 currency codes to price configuration objects. At least one currency is required.

```json theme={"system"}
{
  "USD": {
    "amount": "29.00",
    "taxIncluded": false,
    "taxCategory": "saas"
  },
  "EUR": {
    "amount": "27.00",
    "taxIncluded": true,
    "taxCategory": "saas"
  }
}
```

| Field         | Type    | Description                                                |
| ------------- | ------- | ---------------------------------------------------------- |
| `amount`      | string  | Price as a display format string (e.g., "29.00" = \$29.00) |
| `taxIncluded` | boolean | Whether the amount already includes tax                    |
| `taxCategory` | string  | Tax category for tax calculation                           |

**Supported `taxCategory` values:**

| Value                  | Description                  |
| ---------------------- | ---------------------------- |
| `digital_goods`        | General digital goods        |
| `saas`                 | Software as a Service        |
| `software`             | Downloadable software        |
| `ebook`                | Electronic books             |
| `online_course`        | Online courses and education |
| `consulting`           | Consulting services          |
| `professional_service` | Professional services        |

## Media Item Format

```json theme={"system"}
{
  "type": "image",
  "url": "https://example.com/product.png",
  "alt": "Product screenshot",
  "thumbnail": "https://example.com/product-thumb.png"
}
```

| Field       | Type   | Description                                                 |
| ----------- | ------ | ----------------------------------------------------------- |
| `type`      | string | `image` or `video`                                          |
| `url`       | string | Media URL                                                   |
| `alt`       | string | Alt text for accessibility                                  |
| `thumbnail` | string | Thumbnail URL (optional for images, recommended for videos) |

## Example Request

<CodeGroup>
  ```typescript SDK theme={"system"}
  import { WaffoPancake, TaxCategory } from "@waffo/pancake-ts";

  const client = new WaffoPancake({
    merchantId: process.env.WAFFO_MERCHANT_ID!,
    privateKey: process.env.WAFFO_PRIVATE_KEY!,
  });

  const { product } = await client.onetimeProducts.create({
    storeId: "STO_2aUyqjCzEIiEcYMKj7TZtw",
    name: "Premium Template Pack",
    description: "50 premium design templates for your next project.",
    prices: {
      USD: { amount: "49.00", taxIncluded: false, taxCategory: TaxCategory.DigitalGoods },
      EUR: { amount: "45.00", taxIncluded: true, taxCategory: TaxCategory.DigitalGoods },
    },
    media: [
      { type: "image", url: "https://example.com/templates-preview.png", alt: "Template preview" },
    ],
    successUrl: "https://example.com/thank-you",
    metadata: { category: "design", fileCount: "50" },
  });
  ```

  ```bash cURL theme={"system"}
  MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw"
  TIMESTAMP=$(date +%s)
  BODY='{
    "storeId": "STO_2aUyqjCzEIiEcYMKj7TZtw",
    "name": "Premium Template Pack",
    "description": "50 premium design templates for your next project.",
    "prices": {
      "USD": { "amount": "49.00", "taxIncluded": false, "taxCategory": "digital_goods" },
      "EUR": { "amount": "45.00", "taxIncluded": true, "taxCategory": "digital_goods" }
    },
    "successUrl": "https://example.com/thank-you"
  }'
  BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0)
  CANONICAL_REQUEST="POST
  /v1/actions/onetime-product/create-product
  $TIMESTAMP
  $BODY_HASH"

  SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0)

  curl -X POST "https://api.waffo.ai/v1/actions/onetime-product/create-product" \
    -H "Content-Type: application/json" \
    -H "X-Merchant-Id: $MERCHANT_ID" \
    -H "X-Timestamp: $TIMESTAMP" \
    -H "X-Signature: $SIGNATURE" \
    -d "$BODY"
  ```

  ```python Python theme={"system"}
  import hashlib, time, base64, json, requests
  from cryptography.hazmat.primitives import hashes, serialization
  from cryptography.hazmat.primitives.asymmetric import padding

  merchant_id = "MER_2aUyqjCzEIiEcYMKj7TZtw"
  timestamp = str(int(time.time()))
  body = {
      "storeId": "STO_2aUyqjCzEIiEcYMKj7TZtw",
      "name": "Premium Template Pack",
      "description": "50 premium design templates for your next project.",
      "prices": {
          "USD": {"amount": "49.00", "taxIncluded": False, "taxCategory": "digital_goods"},
          "EUR": {"amount": "45.00", "taxIncluded": True, "taxCategory": "digital_goods"},
      },
      "successUrl": "https://example.com/thank-you",
  }

  body_json = json.dumps(body, separators=(",", ":"))
  body_hash = base64.b64encode(hashlib.sha256(body_json.encode()).digest()).decode()
  path = "/v1/actions/onetime-product/create-product"
  canonical = f"POST\n{path}\n{timestamp}\n{body_hash}"

  with open("private_key.pem", "rb") as f:
      private_key = serialization.load_pem_private_key(f.read(), password=None)

  signature = base64.b64encode(
      private_key.sign(canonical.encode(), padding.PKCS1v15(), hashes.SHA256())
  ).decode()

  resp = requests.post(
      f"https://api.waffo.ai{path}",
      headers={
          "Content-Type": "application/json",
          "X-Merchant-Id": merchant_id,
          "X-Timestamp": timestamp,
          "X-Signature": signature,
      },
      data=body_json,
  )
  print(resp.json())
  ```
</CodeGroup>

## Success Response (200)

```json theme={"system"}
{
  "data": {
    "product": {
      "id": "PROD_3kF9mNpQrStUvWxYz1A2bC",
      "storeId": "STO_2aUyqjCzEIiEcYMKj7TZtw",
      "name": "Premium Template Pack",
      "description": "50 premium design templates for your next project.",
      "prices": {
        "USD": { "amount": "49.00", "taxCategory": "digital_goods" },
        "EUR": { "amount": "45.00", "taxCategory": "digital_goods" }
      },
      "media": [
        { "type": "image", "url": "https://example.com/templates-preview.png", "alt": "Template preview" }
      ],
      "successUrl": "https://example.com/thank-you",
      "metadata": { "category": "design", "fileCount": "50" },
      "status": "active",
      "createdAt": "2026-01-15T10:30:00.000Z",
      "updatedAt": "2026-01-15T10:30:00.000Z"
    }
  }
}
```

## Response Fields

The response is wrapped in `data.product`. The product object is a flattened detail view combining product and version fields.

| Field         | Type           | Description                                               |
| ------------- | -------------- | --------------------------------------------------------- |
| `id`          | string         | Product ID (`PROD_xxx`)                                   |
| `storeId`     | string         | Store ID (`STO_xxx`)                                      |
| `name`        | string         | Product name (from current version)                       |
| `description` | string \| null | Product description (from current version)                |
| `prices`      | object         | Multi-currency pricing map (see below)                    |
| `media`       | array          | Media items (from current version)                        |
| `successUrl`  | string \| null | Success redirect URL (from current version)               |
| `metadata`    | object         | Custom metadata (from current version)                    |
| `status`      | string         | Status in the current environment: `active` or `inactive` |
| `createdAt`   | string         | Product creation timestamp (ISO 8601)                     |
| `updatedAt`   | string         | Product last update timestamp (ISO 8601)                  |

**Response price object** (per currency):

| Field         | Type   | Description                                      |
| ------------- | ------ | ------------------------------------------------ |
| `amount`      | string | Price as a display format string (e.g., "49.00") |
| `taxCategory` | string | Tax category for tax calculation                 |

<Note>
  The `status` field reflects the environment you are operating in. When created in the **test** environment (default), status is `active`. When created in the **production** environment, the production status is returned.
</Note>

## Errors

> **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts).

| Status | `errors[0].message`                      | What it means                                                                          | Recommended handling                                      |
| ------ | ---------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| 400    | `Missing required field: storeId`        | `storeId` not provided                                                                 | Fix the body, resubmit                                    |
| 400    | `Invalid ID format`                      | The provided Short ID could not be decoded                                             | Fix the `storeId` format, resubmit                        |
| 400    | `Missing required field: name`           | `name` not provided                                                                    | Fix the body, resubmit                                    |
| 400    | `Prices must have at least one currency` | `prices` is missing or an empty object                                                 | Provide at least one currency entry, resubmit             |
| 400    | `Invalid currency code`                  | Currency code must be exactly 3 uppercase letters (ISO 4217, e.g. `USD`, `EUR`, `JPY`) | Use a valid ISO 4217 code, resubmit                       |
| 400    | `Invalid amount`                         | Amount must be a positive number string (e.g. `"9.99"`, `"100"`)                       | Use a positive number string, resubmit                    |
| 404    | `Store not found`                        | Store does not exist or is not accessible                                              | Verify the `storeId` belongs to your merchant account     |
| 500    | `Internal server error`                  | Unexpected server-side failure                                                         | Retry with exponential backoff (start 5s, max 3 attempts) |
