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

# Products Webhook

> Complete specification for the products pricing webhook. Implement this endpoint to provide real-time prices and stock availability.

The products webhook is called by VIZOCHOK when the AI needs current prices and availability for a set of products. Your endpoint receives a list of SKUs and returns pricing data for each.

***

## Request Format

**Method**: `POST`

**Headers**:

| Header                 | Value                    |
| ---------------------- | ------------------------ |
| `Content-Type`         | `application/json`       |
| `X-VIZOCHOK-Signature` | `sha256=<hex_digest>`    |
| `X-VIZOCHOK-Timestamp` | Unix timestamp (seconds) |

**Body**:

```json theme={null}
{
  "skus": ["milk-001", "milk-002", "milk-003"],
  "store_id": "store-1"
}
```

<ParamField body="skus" type="string[]" required>
  List of product SKUs to look up. Typically 1-10 SKUs per request, matching what the AI found in the catalog search.
</ParamField>

<ParamField body="store_id" type="string" required>
  Store identifier. Use this to return location-specific pricing if your stores have different prices.
</ParamField>

***

## Response Format

Return a JSON object with a `products` array. Each product must include at minimum `sku`, `price`, and `in_stock`:

```json theme={null}
{
  "products": [
    {
      "sku": "milk-001",
      "price": 45.90,
      "in_stock": true
    },
    {
      "sku": "milk-002",
      "price": 52.00,
      "in_stock": true,
      "promo_price": 42.00,
      "promo_label": "-19%"
    },
    {
      "sku": "milk-003",
      "price": 38.50,
      "in_stock": false
    }
  ]
}
```

### Response Fields

<ResponseField name="products" type="array" required>
  Array of product pricing objects.

  <ResponseField name="sku" type="string" required>
    Product SKU (must match a requested SKU).
  </ResponseField>

  <ResponseField name="price" type="number" required>
    Current retail price.
  </ResponseField>

  <ResponseField name="in_stock" type="boolean" required>
    Whether the product is currently available. Products with `in_stock: false` are filtered out and not shown to the customer.
  </ResponseField>

  <ResponseField name="promo_price" type="number">
    Promotional/discounted price. When present, the AI highlights the discount to the customer.
  </ResponseField>

  <ResponseField name="promo_label" type="string">
    Promotional label displayed alongside the price (e.g., `"-19%"`, `"SALE"`, `"2 for 1"`).
  </ResponseField>
</ResponseField>

### Important Behavior

* **Missing SKUs**: If a requested SKU is not included in your response, VIZOCHOK treats it as unavailable and excludes it from results.
* **Out of stock**: Products with `in_stock: false` are filtered out -- the customer will not see them.
* **Promotions**: When `promo_price` is present, the AI automatically mentions the discount (e.g., "This milk is on sale: 42.00 instead of 52.00").

***

## Handler Examples

<CodeGroup>
  ```python Python (FastAPI) theme={null}
  from fastapi import FastAPI, Request, HTTPException
  from pydantic import BaseModel
  import hmac
  import hashlib
  import time

  app = FastAPI()

  WEBHOOK_SECRET = "your_webhook_secret"

  def verify_signature(request: Request, body: bytes):
      sig_header = request.headers.get("x-vizochok-signature", "")
      timestamp = request.headers.get("x-vizochok-timestamp", "")

      if not sig_header.startswith("sha256="):
          raise HTTPException(401, "Missing signature")

      # Check timestamp (5-minute tolerance)
      if timestamp:
          if abs(time.time() - int(timestamp)) > 300:
              raise HTTPException(401, "Timestamp expired")
          signed_payload = f"{timestamp}.".encode() + body
      else:
          signed_payload = body

      expected = hmac.new(
          WEBHOOK_SECRET.encode(), signed_payload, hashlib.sha256
      ).hexdigest()

      if not hmac.compare_digest(sig_header[7:], expected):
          raise HTTPException(401, "Invalid signature")


  class CheckProductsRequest(BaseModel):
      skus: list[str]
      store_id: str


  @app.post("/api/vizochok/check-products")
  async def check_products(req: CheckProductsRequest, request: Request):
      body = await request.body()
      verify_signature(request, body)

      # Look up products in your database
      products = []
      for sku in req.skus:
          product = await db.get_product(sku, store_id=req.store_id)
          if product:
              item = {
                  "sku": sku,
                  "price": product.price,
                  "in_stock": product.stock > 0,
              }
              if product.promo_price:
                  item["promo_price"] = product.promo_price
                  item["promo_label"] = product.promo_label
              products.append(item)

      return {"products": products}
  ```

  ```javascript Node.js (Express) theme={null}
  const express = require('express');
  const crypto = require('crypto');
  const app = express();

  const WEBHOOK_SECRET = 'your_webhook_secret';

  // Parse raw body for signature verification
  app.use('/api/vizochok', express.json({
    verify: (req, res, buf) => { req.rawBody = buf; }
  }));

  function verifySignature(req) {
    const sigHeader = req.headers['x-vizochok-signature'] || '';
    const timestamp = req.headers['x-vizochok-timestamp'] || '';

    if (!sigHeader.startsWith('sha256=')) {
      throw new Error('Missing signature');
    }

    // Check timestamp (5-minute tolerance)
    if (timestamp) {
      const ts = parseInt(timestamp, 10);
      if (Math.abs(Date.now() / 1000 - ts) > 300) {
        throw new Error('Timestamp expired');
      }
    }

    const signedPayload = timestamp
      ? Buffer.concat([Buffer.from(`${timestamp}.`), req.rawBody])
      : req.rawBody;

    const expected = crypto
      .createHmac('sha256', WEBHOOK_SECRET)
      .update(signedPayload)
      .digest('hex');

    if (!crypto.timingSafeEqual(
      Buffer.from(sigHeader.slice(7)),
      Buffer.from(expected)
    )) {
      throw new Error('Invalid signature');
    }
  }

  app.post('/api/vizochok/check-products', async (req, res) => {
    try {
      verifySignature(req);
    } catch (err) {
      return res.status(401).json({ error: err.message });
    }

    const { skus, store_id } = req.body;

    // Look up products in your database
    const products = [];
    for (const sku of skus) {
      const product = await db.getProduct(sku, store_id);
      if (product) {
        const item = {
          sku,
          price: product.price,
          in_stock: product.stock > 0,
        };
        if (product.promoPrice) {
          item.promo_price = product.promoPrice;
          item.promo_label = product.promoLabel;
        }
        products.push(item);
      }
    }

    res.json({ products });
  });
  ```
</CodeGroup>

***

## Testing

Test your products webhook using the built-in test in the [Admin Panel](https://app.vizochok.com) under **Settings > Webhooks**. Click "Test" to send a test request with sample SKUs to your configured `products_url`.

You can also test locally using curl:

```bash theme={null}
curl -X POST http://localhost:8081/api/vizochok/check-products \
  -H "Content-Type: application/json" \
  -d '{
    "skus": ["milk-001", "bread-042"],
    "store_id": "store-1"
  }'
```

***

## Performance Tips

<Tip>
  Keep your products webhook fast (under 2 seconds). The customer is waiting for search results while this call happens. The default timeout is 5 seconds, but faster responses mean a better experience.
</Tip>

* **Batch efficiently**: You will receive 1-10 SKUs per request. Use a single database query with `IN` clause rather than individual lookups.
* **Cache prices**: If your pricing system is slow, consider a short-lived cache (30-60 seconds) for price lookups.
* **Return early**: Only include `promo_price` and `promo_label` when there is actually a promotion. Omitting optional fields is fine.
