{
  "openapi": "3.1.0",
  "info": {
    "title": "SalSmart Israeli Grocery Price Search",
    "version": "2.0.0",
    "description": "Israeli supermarket product metadata and per-branch ILS prices from public retailer price-transparency files. Basic search is free with no API key: up to 20 products, 60 valid attempts per fixed UTC minute and 500 per UTC day per network address. The prepaid agent API supports up to 100 products: a one-time ILS 19 pack provides 1,000 successful-search credits, with no automatic renewal. Payment is through Stripe Checkout; access is granted only after verified payment. One HTTP 200 search, including zero matches, costs one credit; idempotent replays within 24 hours cost zero. Coverage is partial and is reported live by /api/data-status. Prices do not confirm inventory. Source publication, successful fetch and retailer price-change dates are separate. The legacy x402 endpoint remains TESTNET ONLY."
  },
  "servers": [
    {
      "url": "https://salsmart.com"
    }
  ],
  "externalDocs": {
    "description": "Agent guide: free quotas, real prepaid access, provenance and legacy x402 testnet",
    "url": "https://salsmart.com/agents"
  },
  "tags": [
    {
      "name": "Products",
      "description": "Product search with per-branch prices in ILS."
    },
    {
      "name": "Sources",
      "description": "Live source provenance and refresh status for covered branches."
    },
    {
      "name": "Billing",
      "description": "One-time prepaid API credit purchase and private key claim."
    }
  ],
  "paths": {
    "/api/product-search": {
      "get": {
        "operationId": "searchProducts",
        "summary": "Free search for Israeli grocery products and branch prices",
        "description": "Free basic search; no registration, key or payment. q is 2–100 characters and limit is 1–20 (default 20). At most 60 syntactically valid requests per fixed UTC minute and 500 per UTC day per network address; shared networks share the allowance. Invalid queries do not consume quota. Valid admitted attempts count even if no product matches or a later database error occurs. The server stores only a daily, purpose-scoped HMAC quota identifier, never the raw address or query. Respect Retry-After on 429. Search terms are ANDed; barcode text matching is not guaranteed exact; verify returned barcodes. No pagination. Coverage is partial.",
        "tags": [
          "Products"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/Query"
          },
          {
            "$ref": "#/components/parameters/Limit"
          }
        ],
        "responses": {
          "200": {
            "description": "Search completed. Empty results use count 0 and products []. Results are partial coverage, not an inventory or real-time-price guarantee.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductSearchResponse"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Fixed free-search policies: 60 per minute and 500 per UTC day.",
                "schema": {
                  "type": "string"
                },
                "example": "60;w=60, 500;w=86400"
              },
              "RateLimit-Limit": {
                "description": "Limit for the currently controlling quota window.",
                "schema": {
                  "type": "string"
                },
                "example": "60"
              },
              "RateLimit-Remaining": {
                "description": "Remaining admissions in the most restrictive active window; not a service guarantee.",
                "schema": {
                  "type": "string"
                },
                "example": "59"
              },
              "RateLimit-Reset": {
                "description": "Delay in seconds until that controlling window resets.",
                "schema": {
                  "type": "string"
                },
                "example": "45"
              },
              "Cache-Control": {
                "description": "Responses and quota metadata are private to this request and not cached.",
                "schema": {
                  "type": "string"
                },
                "example": "no-store"
              }
            }
          },
          "400": {
            "description": "Invalid or missing q, or limit outside 1–20. No quota admission is consumed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Free minute or daily allowance exhausted. This denied request increments neither counter. Use the prepaid agent API for larger access.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "description": "Fixed free-search policies: 60 per minute and 500 per UTC day.",
                "schema": {
                  "type": "string"
                },
                "example": "60;w=60, 500;w=86400"
              },
              "RateLimit-Limit": {
                "description": "Limit for the currently controlling quota window.",
                "schema": {
                  "type": "string"
                },
                "example": "60"
              },
              "RateLimit-Remaining": {
                "description": "Remaining admissions in the most restrictive active window; not a service guarantee.",
                "schema": {
                  "type": "string"
                },
                "example": "59"
              },
              "RateLimit-Reset": {
                "description": "Delay in seconds until that controlling window resets.",
                "schema": {
                  "type": "string"
                },
                "example": "45"
              },
              "Cache-Control": {
                "description": "Responses and quota metadata are private to this request and not cached.",
                "schema": {
                  "type": "string"
                },
                "example": "no-store"
              },
              "Retry-After": {
                "description": "Wait at least this many seconds before trying free search again.",
                "schema": {
                  "type": "string"
                },
                "example": "45"
              }
            }
          },
          "503": {
            "description": "Free quota configuration, trusted edge identity or database/search service unavailable. Do not interpret this as no matching products.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/paid/product-search": {
      "get": {
        "operationId": "searchProductsX402Testnet",
        "summary": "Optional x402 v2 testnet search (0.01 testnet USDC)",
        "description": "LEGACY TESTNET ONLY: x402 v2 on Base Sepolia (eip155:84532), requesting 0.01 testnet USDC per request. This does not purchase real API credits. For production paid access use /api/billing/checkout and /api/v1/search, documented at https://salsmart.com/agents. The free endpoint remains available with its basic limits. Read the Base64 PAYMENT-REQUIRED challenge and retry with PAYMENT-SIGNATURE only after user authorization and a budget; do not hard-code recipient/asset details.",
        "tags": [
          "Products"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/X402Query"
          },
          {
            "$ref": "#/components/parameters/PaidLimit"
          },
          {
            "name": "PAYMENT-SIGNATURE",
            "in": "header",
            "required": false,
            "description": "Base64-encoded x402 v2 payment payload prepared by a compatible client using the current 402 challenge. Omit to inspect testnet payment requirements.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Search completed. Empty results use count 0 and products []. Results are partial coverage, not an inventory or real-time-price guarantee.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/X402ProductSearchResponse"
                }
              }
            },
            "headers": {
              "PAYMENT-RESPONSE": {
                "description": "May be present after settlement; Base64-encoded x402 settlement response.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Missing search query.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Testnet payment required or supplied payment rejected. The challenge header contains the current x402 version, network, asset, amount, and recipient requirements.",
            "headers": {
              "PAYMENT-REQUIRED": {
                "description": "Base64-encoded x402 v2 PaymentRequired JSON. Testnet Base Sepolia (eip155:84532); requested price is 0.01 testnet USDC. Follow the values in the actual challenge.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Search or payment processing failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Testnet payment receiving address is not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/data-status": {
      "get": {
        "operationId": "getPriceDataStatus",
        "summary": "Live source-file provenance and refresh status for covered branches",
        "description": "Returns uncached branch-level source metadata for active-chain branches whose positive-price records are served. checked_at is the status request time; last_success_at is when the source was successfully fetched; source_published_at is the publication timestamp from the source filename. These are separate from per-item update_date. A fresh store currently requires a successful fetch in the last 12 hours and a file published in the last 72 hours; unknown/future timestamps are not fresh. A six-hour refresh schedule is a cadence, not a guarantee of a successful fetch. Coverage is partial, not all Israeli stores; the live counts establish the actual current coverage. Check status and missing timestamps for individual branches; a failed refresh retains previous data.",
        "tags": [
          "Sources"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Current coverage, freshness counts and per-branch source metadata. Read the actual counts rather than assuming every branch refreshed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataStatus"
                }
              }
            },
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "no-store"
              }
            }
          },
          "503": {
            "description": "Source metadata is temporarily unavailable. Do not infer that data is fresh.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/search": {
      "get": {
        "operationId": "searchProductsPrepaid",
        "summary": "Production prepaid agent search — up to 100 products per credit",
        "description": "Authenticate with a live Salsmart Bearer key and send a unique Idempotency-Key. q must be 2–100 characters; limit 1–100, default 20. Every newly completed HTTP 200 search, including zero results, consumes exactly one prepaid credit. Validation, authentication and internal failures do not consume credits. Retry uncertain requests with the SAME key and parameters. Exact response replay lasts 24 hours from completion; afterwards the retained idempotency tombstone returns 409 idempotency_expired without another charge. A changed query under an unexpired key returns 409 idempotency_conflict. To request new data, use a new key. Read Idempotency-Expires-At. A one-time ILS 19 pack provides 1,000 credits with no automatic renewal. Purchases require authorization and a budget; never charge a user automatically.",
        "tags": [
          "Products"
        ],
        "security": [
          {
            "ApiKeyBearer": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Query"
          },
          {
            "$ref": "#/components/parameters/PaidLimit"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful paid search, including empty results, costs one prepaid credit. Repeating the same key and parameters within 24 hours replays exact data and costs zero.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PrepaidProductSearchResponse"
                }
              }
            },
            "headers": {
              "Cache-Control": {
                "description": "Private response; no browser/CDN caching.",
                "schema": {
                  "type": "string"
                },
                "example": "no-store"
              },
              "Idempotency-Key": {
                "description": "The request idempotency key.",
                "schema": {
                  "type": "string"
                }
              },
              "Idempotency-Expires-At": {
                "description": "ISO 8601 UTC end of the 24-hour exact-response replay window. This does not expire the API key.",
                "schema": {
                  "type": "string"
                }
              },
              "Idempotent-Replayed": {
                "description": "true when replayed; false for a newly charged search.",
                "schema": {
                  "type": "string"
                },
                "example": "false"
              },
              "X-Request-Id": {
                "description": "Stable ID for this completed search.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Credits-Remaining": {
                "description": "Account credit balance observed with the response.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Credits-Charged": {
                "description": "1 for a new successful search, 0 for an idempotent replay.",
                "schema": {
                  "type": "string"
                },
                "example": "1"
              }
            }
          },
          "400": {
            "description": "Invalid q, limit or required Idempotency-Key; no credit charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, expired, disabled or revoked API key; no credit charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "credits_exhausted: purchase another credit pack; no automatic charge or renewal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "idempotency_conflict for changed parameters, or idempotency_expired after the 24-hour replay window. No new credit charged. Use a new key only for a new paid search.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Serialized result exceeds the response cache safety limit. Retry a smaller limit with a new Idempotency-Key; no credit charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Temporary service failure. Retry with the same Idempotency-Key to avoid duplicate billing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/usage": {
      "get": {
        "operationId": "getPrepaidUsage",
        "summary": "Private prepaid credit balance and usage; no credit charge",
        "description": "Requires the same Bearer key. This operation is free and has no idempotency requirement. Returns account ID, key prefix, purchased and consumed credits, remaining balance, per-search cost and checked_at.",
        "tags": [
          "Billing"
        ],
        "security": [
          {
            "ApiKeyBearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Current private balance and cumulative credit usage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PrepaidUsage"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or inactive Bearer key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Usage is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/billing/checkout": {
      "get": {
        "operationId": "getPrepaidPlan",
        "summary": "Check whether production credit purchases are enabled",
        "tags": [
          "Billing"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Public configuration status. enabled false means new purchases are not available.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "enabled",
                    "plan"
                  ],
                  "properties": {
                    "enabled": {
                      "type": "boolean"
                    },
                    "plan": {
                      "type": "object",
                      "required": [
                        "id",
                        "currency",
                        "amount",
                        "credits"
                      ],
                      "properties": {
                        "id": {
                          "type": "string",
                          "const": "starter"
                        },
                        "currency": {
                          "type": "string",
                          "const": "ils"
                        },
                        "amount": {
                          "type": "integer",
                          "const": 1900,
                          "description": "19.00 ILS, in agorot. One-time pack; no automatic renewal."
                        },
                        "credits": {
                          "type": "integer",
                          "const": 1000
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "startPrepaidPurchase",
        "summary": "Start an authorized one-time ILS 19 purchase of 1,000 API credits",
        "description": "Send JSON {\"plan\":\"starter\"}. For an existing-account top-up, also send that account's Bearer API key. No key starts a new account. After user payment authorization, follow checkout_url to Stripe. Save purchase_id and claim_token securely before redirecting; they are needed to claim access and must never be placed in URLs, logs or analytics. This call alone neither pays nor grants credits. Only a verified settled payment grants credit. There is no subscription or automatic renewal. Initiation is limited to 10 attempts per hour per network address.",
        "tags": [
          "Billing"
        ],
        "security": [
          {},
          {
            "ApiKeyBearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "plan"
                ],
                "properties": {
                  "plan": {
                    "type": "string",
                    "const": "starter"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "A purchase was created; use the returned checkout_url to complete authorized payment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "purchase_id",
                    "claim_token",
                    "checkout_url"
                  ],
                  "properties": {
                    "purchase_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "claim_token": {
                      "type": "string",
                      "description": "Private 43-character one-time claim secret. Save securely; only a hash is stored server-side."
                    },
                    "checkout_url": {
                      "type": "string",
                      "format": "uri",
                      "description": "Stripe-hosted payment page for this purchase reference."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown plan or invalid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "A supplied top-up API key is invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Cross-origin checkout request rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Request exceeds 4 KiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Content-Type must be application/json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many checkout attempts; retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Live purchases are not configured or temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/billing/claim": {
      "post": {
        "operationId": "claimPrepaidAccess",
        "summary": "Claim a paid purchase or confirm an existing-account top-up",
        "description": "Send the private purchase_id and claim_token from checkout initiation in the JSON body. Poll 202 using Retry-After. New-account API keys are returned exactly once after verified payment: save the key securely. An already claimed key cannot be recovered by replaying the claim. Top-ups retain the existing key and return the updated balance. Never place claim secrets or API keys in URLs, logs, analytics or public browser storage.",
        "tags": [
          "Billing"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "purchase_id",
                  "claim_token"
                ],
                "properties": {
                  "purchase_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "claim_token": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9_-]{43}$"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "New purchase: status claimed and api_key shown once; top-up: status credited with no new key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "credits_remaining"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "claimed",
                        "credited"
                      ]
                    },
                    "api_key": {
                      "type": "string",
                      "description": "Returned once for a new account only."
                    },
                    "key_prefix": {
                      "type": "string"
                    },
                    "credits_added": {
                      "type": "integer"
                    },
                    "credits_remaining": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Payment confirmation pending; poll after the header delay.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait.",
                "schema": {
                  "type": "string"
                },
                "example": "3"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "pending"
                    },
                    "retry_after": {
                      "type": "integer",
                      "const": 3
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid purchase ID or claim token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Payment review, purchase not ready, or key_already_claimed. No extra key is issued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "Checkout expired. Start a new purchase.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Request exceeds 4 KiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Content-Type must be application/json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Claim service is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "Query": {
        "name": "q",
        "in": "query",
        "required": true,
        "description": "Product name, manufacturer or barcode text; Hebrew supported. Whitespace is normalized; 2–100 characters. Up to five terms are ANDed. Free search also accepts search as a legacy alias when q is absent; paid v1 requires q.",
        "schema": {
          "type": "string",
          "minLength": 2,
          "maxLength": 100
        },
        "example": "חלב תנובה"
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Maximum products requested before filtering unavailable offers. Free search accepts an integer from 1 to 20; default 20. Invalid or larger values return 400.",
        "schema": {
          "type": "integer",
          "default": 20,
          "minimum": 1,
          "maximum": 20
        },
        "example": 5
      },
      "PaidLimit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Paid v1 search accepts an integer from 1 to 100; default 20.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 20
        },
        "example": 5
      },
      "X402Query": {
        "name": "q",
        "in": "query",
        "required": true,
        "description": "Legacy TESTNET search term; accepts search as an alias. Prefer /api/v1/search for real prepaid agent access.",
        "schema": {
          "type": "string",
          "minLength": 1
        },
        "example": "חלב תנובה"
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "description": "Unique request identifier. Reuse the same key AND parameters when retrying an uncertain request. Exact response replay is available for 24 hours from completion; an expired key returns 409 and is never billed again. A new paid search requires a new key.",
        "schema": {
          "type": "string",
          "minLength": 8,
          "maxLength": 128,
          "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$"
        },
        "example": "agent-search-000001"
      }
    },
    "schemas": {
      "ProductSearchResponse": {
        "type": "object",
        "required": [
          "query",
          "count",
          "products"
        ],
        "properties": {
          "query": {
            "type": "string",
            "description": "The submitted query."
          },
          "count": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of products actually returned, not the total number of matches."
          },
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Product"
            }
          }
        }
      },
      "Product": {
        "type": "object",
        "required": [
          "barcode",
          "name",
          "manufacturer",
          "unit_of_measure",
          "image_url",
          "offers"
        ],
        "properties": {
          "barcode": {
            "type": "string",
            "description": "Source identifier. Preserve leading zeros; not every identifier is a validated GTIN."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Source product name; can be abbreviated or missing. Do not invent expanded names."
          },
          "manufacturer": {
            "type": [
              "string",
              "null"
            ],
            "description": "Source manufacturer, when known."
          },
          "unit_of_measure": {
            "type": [
              "string",
              "null"
            ],
            "description": "Source measurement basis, not necessarily package size. Do not infer unit price or package quantity from this alone."
          },
          "image_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Product image URL when known."
          },
          "offers": {
            "type": "array",
            "minItems": 1,
            "description": "Positive-price offers from active chains, ordered by price ascending. Coverage is partial.",
            "items": {
              "$ref": "#/components/schemas/PriceOffer"
            }
          }
        }
      },
      "PriceOffer": {
        "type": "object",
        "required": [
          "price",
          "update_date",
          "store_id",
          "chain_id",
          "store_name",
          "store_city",
          "chain_name"
        ],
        "properties": {
          "price": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "Reported price in ILS for this product at this branch. Does not confirm stock, delivery, or promotion eligibility."
          },
          "update_date": {
            "type": [
              "string",
              "null"
            ],
            "description": "Source price-change timestamp, often YYYY-MM-DD HH:mm:ss, or null when unknown. Not the time a file was fetched or the price was checked. The source may omit a timezone. Preserve old or missing values; never substitute today's date.",
            "examples": [
              null
            ]
          },
          "store_id": {
            "type": "string",
            "description": "Branch identifier; use with chain_id. It is not globally unique."
          },
          "chain_id": {
            "type": "string",
            "description": "Chain identifier; use with store_id to identify the branch."
          },
          "store_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Source branch name when known."
          },
          "store_city": {
            "type": [
              "string",
              "null"
            ],
            "description": "Source city when known. Null does not establish a location."
          },
          "chain_name": {
            "type": "string",
            "description": "Source chain name."
          },
          "source_file": {
            "type": [
              "string",
              "null"
            ],
            "description": "Filename of the last successfully imported full-price source for this exact chain and store, when recorded."
          },
          "source_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Public provenance URL for the source file, listing, or resolver. Credentials and signed download tokens are omitted. FTPS URLs may require a compatible client."
          },
          "source_published_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Source-file publication timestamp from the filename, with an explicit timezone. This is not the per-item price-change date or the fetch time.",
            "format": "date-time"
          },
          "fetched_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Time this branch source file was last fetched and imported successfully, in ISO 8601 with an explicit timezone. Never derive this from update_date. Missing or null means unknown.",
            "format": "date-time"
          },
          "refresh_status": {
            "type": [
              "string",
              "null"
            ],
            "description": "Outcome of the latest branch refresh attempt; failed means the previous successful data is retained. A status alone does not prove freshness; inspect timestamps and /api/data-status."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "details": {
            "type": "string",
            "description": "Optional additional diagnostic information."
          },
          "message": {
            "type": "string",
            "description": "Safe user-facing explanation; internal database errors are not exposed by free/v1/billing handlers."
          }
        }
      },
      "DataSourceStatus": {
        "type": "object",
        "required": [
          "chain_id",
          "store_id",
          "chain_name",
          "source_file",
          "source_url",
          "source_published_at",
          "last_attempt_at",
          "last_success_at",
          "price_count",
          "status",
          "last_error"
        ],
        "properties": {
          "chain_id": {
            "type": "string"
          },
          "store_id": {
            "type": "string"
          },
          "chain_name": {
            "type": "string"
          },
          "source_file": {
            "type": [
              "string",
              "null"
            ],
            "description": "Last successful source filename."
          },
          "source_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Public source provenance without credentials or signed download tokens."
          },
          "source_published_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Last successful source-file publication time, with timezone.",
            "format": "date-time"
          },
          "last_attempt_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Most recent refresh attempt time, with timezone.",
            "format": "date-time"
          },
          "last_success_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Most recent successful source fetch/import time; aliases offers[].fetched_at.",
            "format": "date-time"
          },
          "price_count": {
            "type": "integer",
            "minimum": 0,
            "description": "Positive-price records currently served for this branch."
          },
          "status": {
            "type": "string",
            "description": "Last refresh outcome, or unknown when no source tracking exists."
          },
          "last_error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Generic failure notice. Private operational diagnostics are not exposed."
          }
        }
      },
      "DataStatus": {
        "type": "object",
        "required": [
          "checked_at",
          "updated_at",
          "fresh_stores",
          "total_stores",
          "stale_stores",
          "sources"
        ],
        "properties": {
          "checked_at": {
            "type": "string",
            "format": "date-time",
            "description": "Time this live status response was generated."
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Latest successful source fetch/import across covered branches. It does not mean all branches refreshed at that time.",
            "format": "date-time"
          },
          "fresh_stores": {
            "type": "integer",
            "minimum": 0,
            "description": "Branches meeting the successful-fetch and source-publication freshness bounds."
          },
          "total_stores": {
            "type": "integer",
            "minimum": 0,
            "description": "Branches whose active-chain positive prices are currently served."
          },
          "stale_stores": {
            "type": "integer",
            "minimum": 0,
            "description": "Covered branches with stale, missing, or unverifiable source timing."
          },
          "sources": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DataSourceStatus"
            }
          }
        }
      },
      "X402ProductSearchResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ProductSearchResponse"
          },
          {
            "type": "object",
            "properties": {
              "x402": {
                "type": "object",
                "description": "Optional settlement metadata for the testnet payment.",
                "properties": {
                  "correlationId": {
                    "type": "string",
                    "description": "Payment attempt correlation identifier."
                  },
                  "transaction": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Testnet settlement transaction identifier, when available."
                  }
                }
              }
            }
          }
        ]
      },
      "PrepaidProductSearchResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ProductSearchResponse"
          },
          {
            "type": "object",
            "required": [
              "request_id",
              "limit",
              "generated_at",
              "billing"
            ],
            "properties": {
              "request_id": {
                "type": "string",
                "format": "uuid"
              },
              "limit": {
                "type": "integer",
                "minimum": 1,
                "maximum": 100
              },
              "generated_at": {
                "type": "string",
                "format": "date-time"
              },
              "billing": {
                "type": "object",
                "required": [
                  "credits",
                  "unit"
                ],
                "properties": {
                  "credits": {
                    "type": "integer",
                    "const": 1,
                    "description": "Original purchased search cost. Replayed response bodies remain unchanged; X-Credits-Charged is 0 on replay."
                  },
                  "unit": {
                    "type": "string",
                    "const": "successful_search"
                  }
                }
              }
            }
          }
        ]
      },
      "PrepaidUsage": {
        "type": "object",
        "required": [
          "account_id",
          "key_prefix",
          "credits_remaining",
          "purchased_credits",
          "consumed_credits",
          "credit_cost_per_search",
          "checked_at"
        ],
        "properties": {
          "account_id": {
            "type": "string"
          },
          "key_prefix": {
            "type": "string",
            "description": "Non-secret key prefix, never the complete API key."
          },
          "credits_remaining": {
            "type": "integer",
            "minimum": 0
          },
          "purchased_credits": {
            "type": "integer",
            "minimum": 0
          },
          "consumed_credits": {
            "type": "integer",
            "minimum": 0
          },
          "credit_cost_per_search": {
            "type": "integer",
            "const": 1
          },
          "checked_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    },
    "securitySchemes": {
      "ApiKeyBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "sk_salsmart_live_…",
        "description": "Send the once-issued API key only in Authorization: Bearer <API_KEY>. Never place keys in URLs, query parameters, logs or analytics."
      }
    }
  }
}
