PVSN — PRICE VARIATION SOFTWARE NETWORK MACHINE-READABLE INTEGRATION MANUAL FOR AI AGENTS ================================================= This document is written for an LLM agent (Claude, GPT, or similar) that has been asked to integrate a merchant's store or tooling with PVSN. It is not marketing material and it is not written for humans. Everything below is derived from the live backend source. When this file and any prose docs disagree, trust this file, then the OpenAPI spec. CONTENTS 1. What PVSN is (one paragraph of context) 2. Base URL, auth, and how to get a key 3. Request/response conventions (errors, pagination, idempotency, rate limits) 4. Endpoint reference by group 4.1 Products 4.2 Customers 4.3 Events (single + batch ingestion) 4.4 Predictions (PDE) 4.5 Offers (accept / dismiss) 4.6 Pricing rules (guardrails) 4.7 Price variation engine (calculate / simulate / batch) 4.8 Dynamic price + price preview 4.9 Analytics 4.10 Webhooks + HMAC signature verification 4.11 API keys 4.12 Bulk operations 4.13 Competitor prices (feed + quota) 4.14 Display channels (digital screens, ESL, social) 4.15 Menu boards (public TV display by token) 4.16 AI assistant (chat) + AI pricing advisor 4.17 Tier-3 engine extras (LTV, elasticity, bundles, forecast) 4.18 Account, billing, usage, and merchant settings 4.19 Real-time (SSE) and the rest of the catalog 5. Embeddable JavaScript SDK 6. Step-by-step workflows (do these in order) 7. Hard rules (never violate) ------------------------------------------------------------------ 1. WHAT PVSN IS ------------------------------------------------------------------ PVSN is a multi-tenant dynamic-pricing platform. A merchant syncs products and customers, then streams interaction events (views, purchases, scans, usage). The Predictive Demand Engine (PDE) computes, per customer x product, a predicted next-purchase date with a confidence score. The Adaptive Pricing Engine (APE) and the Price Variation Engine turn predictions plus 100+ live variables (time of day, weather, demand, inventory level, customer segment, events, competition, product lifecycle) into per-customer offers and dynamic prices. Prices go UP as well as DOWN. Merchant-defined pricing rules (floors, ceilings, max discount, max markup, margin protection) are always enforced server-side. All data is scoped to the authenticated merchant. You can never read or write another merchant's data; merchant_id is derived from the credential, never passed by you. ------------------------------------------------------------------ 2. BASE URL, AUTH, AND HOW TO GET A KEY ------------------------------------------------------------------ Base URL: https://api.pvsnapp.com/v1 Protocol: HTTPS only. JSON bodies (Content-Type: application/json). AUTHENTICATION — two distinct schemes. Do not confuse them. (a) API key — use this for integrations and agents. - A human admin creates it in the PVSN app: Settings -> API Keys (or POST /api-keys with an admin session — see 4.11). - The raw key looks like: pvsn_live_ - It is shown ONCE at creation and stored only as a hash. If lost, rotate it (POST /api-keys/{key_id}/rotate). - Send it on every request as a header: X-API-Key: pvsn_live_xxxxxxxxxxxxxxxxxxxxxxxx - API-key requests act as the merchant's admin user (full access). (b) Bearer session token — dashboard logins only. - Obtained by interactive login (POST /auth/login). Expires. - Sent as: Authorization: Bearer - IMPORTANT: a raw API key placed in the Authorization: Bearer header will be rejected with 401 ("Invalid or expired token"). API keys go in X-API-Key, nothing else. A request with neither header returns: 401 {"detail": "Authentication required: provide Bearer token or X-API-Key"} ------------------------------------------------------------------ 3. CONVENTIONS ------------------------------------------------------------------ ERROR ENVELOPE - Business errors raised by routes: {"detail": ""} - Globally handled errors additionally: {"detail": "...", "error_code": "...", "request_id": "..."} - Validation errors (HTTP 422, FastAPI): {"detail": [{"loc": [...], "msg": "...", "type": "..."}]} - Common status codes: 200/201 success 204 success, empty body (DELETE) 401 bad/missing creds 402 plan limit / expired free trial 404 not found (tenant-scoped — also returned for other tenants' ids) 409 idempotency-key body mismatch 422 validation error 429 rate limited - Every response carries X-Request-ID (include it in support requests). PAGINATION - List endpoints use limit/offset query params, newest-first ordering. /products, /customers: limit 1..1000 (default 50), offset >= 0 /predictions: limit 1..500 (default 100), offset >= 0 /offers: limit 1..500 (default 100), no offset — filter instead - Page until you receive fewer rows than `limit`. IDEMPOTENCY - On any POST/PUT/PATCH you may send: X-Idempotency-Key: - Same key + same credential + same body within 24h => the cached response is replayed (no double-write). Same key with a DIFFERENT body => 409. - Always set this header when retrying after timeouts or 5xx. RATE LIMITS (sliding 60s window, per API-key tier) standard: 120 requests/min (default; also used for Bearer tokens) premium: 600 requests/min enterprise: 2000 requests/min - Every rate-limited response includes: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset - On 429 you also get Retry-After: 60 and a body: {"detail": {"error": "RateLimitExceeded", "message": "...", "retry_after_seconds": 60, "tier": "...", "limit": N, "endpoint": false}} - Some expensive endpoints have stricter per-endpoint caps; honor the headers, not assumptions. Back off exponentially on repeated 429s. PLAN INCLUSIONS & USAGE BILLING (mirrors backend plan config exactly) - Competitor price refreshes (POST /products/{id}/competitors/refresh and POST /competitors/refresh) are a metered plan resource, counted per product refreshed: free / starter / pay-as-you-go: 0 included (requests 402) pro: 200 product-refreshes / month included enterprise: 2,000 / month included; refreshes beyond the inclusion are NOT blocked — they bill as metered overage at $0.30 per product refresh. GET /competitors/quota returns {used, limit, remaining, blocked, overage_allowed, overage_rate, message} for the current period. Manual entry (POST /products/{id}/competitors), CSV import, and manual-provider mode are free and never consume quota. - Claude AI calls are plan-limited (free 10, starter 500, pro 5,000, enterprise 20,000 per month); pay-as-you-go AI calls are billed at $35 per 1,000 calls ($0.035/call). - Exceeding an inclusion returns HTTP 402 with {"detail": {"message", "resource", "current", "limit", "plan_tier"}} — surface the message to the merchant; do not retry around it. ------------------------------------------------------------------ 4. ENDPOINT REFERENCE BY GROUP ------------------------------------------------------------------ All paths below are relative to https://api.pvsnapp.com/v1 4.1 PRODUCTS (/products) POST /products create (201) GET /products?limit&offset list GET /products/{product_id} read PATCH /products/{product_id} partial update (all fields optional) DELETE /products/{product_id} delete (204) Create/read fields: name (str, required, 1..200) price (float, required, >= 0) description (str|null, <=2000) unit_cost (float|null, >= 0) — enables margin protection sku (str|null, <=100) category (str|null, <=200) image_url (str|null, <=500) stock_quantity (int|null, >= 0) low_stock_threshold (int|null) lifecycle_days (int|null, >= 1) — expected days between purchases; PDE fallback when history is thin Read adds: id, created_at, updated_at. NOTE: the price field is named `price` (not base_price). 4.2 CUSTOMERS (/customers) POST /customers create (201) GET /customers?limit&offset list GET /customers/{customer_id} read PATCH /customers/{customer_id} partial update DELETE /customers/{customer_id} delete (204) Create fields (all optional): external_id (str, <=200) — YOUR system's id; set it so you can map back email, name (str, <=200) phone (str, <=50) tags (list) segment, lifetime_value, churn_risk (0..1) — normally computed by PVSN's segmentation service; only set these when seeding. Read adds: id, merchant_id, created_at, last_event_at (timestamp of the customer's most recent event). 4.3 EVENTS (/events) POST /events ingest one event (201) POST /events/batch ingest up to 1000 events in one call (201) EventCreate: customer_id (int, required) product_id (int, required) event_type (required, one of): "view" | "purchase" | "qr_scan" | "add_to_cart" | "usage" | "refill_event" | "nfc_tap" | "breakage_event" metadata (object|null) e.g. {"quantity": 2, "order_id": "A-1001"} timestamp (ISO 8601|null) omit for "now"; SET IT when backfilling history Batch request: {"events": [EventCreate, ... up to 1000]} Batch response: {"total": N, "inserted": M, "errors": [{"index": i, "error": "..."}]} Events are append-only. "purchase" events are what drive predictions — backfill at least 2-3 purchases per customer x product for useful output. 4.4 PREDICTIONS (/predictions) — read-only; computed by the PDE GET /predictions?limit&offset list (newest first) GET /predictions/{customer_id}/{product_id} one pair (404 if none yet) PredictionRead: id, merchant_id, customer_id, product_id predicted_need_date (datetime|null) — when the customer will need the product window_start, window_end (datetime) — uncertainty window around that date confidence (float 0..1) — treat < 0.3 as weak computed_cycle_days (float|null) — learned repurchase cycle last_purchase_date, signals (object), accuracy_score, created_at, updated_at Predictions refresh on a cron (and via POST /cron/pde-refresh). They are not recomputed synchronously when you post an event. 4.5 OFFERS (/offers) — generated by the engine, acted on by you GET /offers?customer_id=&product_id=&status=&limit= list with filters - customer_id set => all offers for that customer (status filter applies) - product_id set => all offers for that product - neither => merchant-wide, capped by limit (max 500) - status values: "active", "accepted", "dismissed", "expired" GET /offers/{offer_id} read one POST /offers/{offer_id}/accept mark accepted (customer took the deal) POST /offers/{offer_id}/dismiss mark dismissed OfferRead: id, merchant_id, customer_id, product_id baseline_price (decimal) dynamic_price (decimal) — the price to show discount_percent (decimal|null; negative dynamics = markup) status (str) trigger (str — why it was generated) reasoning (object|null) message (str|null — customer-facing copy) auto_approved (bool), auto_approval_rule_id, auto_approved_at expires_at (datetime|null — do not honor after this), created_at, updated_at Offers are CREATED by PVSN (PDE/APE + cron + auto-approval rules), not by POSTing to /offers. To influence generation use pricing rules (4.6), auto-approval rules (/auto-approval/rules), or batch generation (POST /offer-features/batch-generate). 4.6 PRICING RULES (/pricing-rules) — guardrails the engine must respect POST /pricing-rules create (201) GET /pricing-rules list GET /pricing-rules/{rule_id} read PUT /pricing-rules/{rule_id} update DELETE /pricing-rules/{rule_id} delete (204) Fields: scope (str, default "global") — "global", or scope to a category (set `category`) or one product (set `product_id`) product_id (int|null) category (str|null) max_discount_percent (decimal, default 20.00) max_urgency_markup_percent (decimal, default 0.00) — max upward move loyalty_weight (decimal, default 0.100) urgency_weight (decimal, default 0.300) price_floor (decimal|null) price_ceiling (decimal|null) active (bool, default true) Inheritance: customer > product > category > global (most specific wins). Margin protection: if a product has unit_cost set, the engine will not price below cost regardless of rules. 4.7 PRICE VARIATION ENGINE (/price-variation) GET /price-variation/calculate/{product_id} ?customer_id= &location= &include_breakdown= => optimal current price for the product. Response keys include: product_id, base_price, final_price (or price), multiplier, adjustments / factors breakdown (omitted when include_breakdown=false), backend ("native" | "legacy" | "legacy:fallback"). NOTE: returns 402 if the merchant's free trial has expired. POST /price-variation/calculate-batch body: {"product_ids": [...], optional customer_id/location} => same math as the single endpoint for many products in one call. Prefer this over looping GET /calculate — one plan-gate, one query. POST /price-variation/simulate body: {product_id (required), customer_id?, customer_segment?, time?, weather?, location?, demand_level?, event?, stock_level?} => what-if price; nothing is persisted. Response echoes "overrides" and sets "simulation": true. GET /price-variation/factors/{product_id} current factor values GET /price-variation/history/{product_id}?days=30 (1..365) GET /price-variation/variables catalog of all pricing variables GET /price-variation/rules engine config for this merchant POST /price-variation/rules update engine config GET /price-variation/weather?location= weather context in use POST /price-variation/weather/rules weather-based rule config GET /price-variation/competitive-position vs. competitor price feed 4.8 DYNAMIC PRICE + PREVIEW GET /price/{product_id}?customer_id= => most recent saved per-customer dynamic price, falling back to the customer's active offer, falling back to base price. Fields: baseline price, dynamic price, delta, factors/reasoning. POST /pricing/preview body: pricing context (product/customer) => PriceQuote without persisting anything. 4.9 ANALYTICS (/analytics, /analytics/* — read-only) GET /analytics/overview (response shape, exact): {"total_customers": int, "total_products": int, "total_events": int, "active_offers": int, "accepted_offers": int, "total_predictions": int, "total_reward_points_issued": float} Other useful GETs (self-describing JSON): /analytics/predictions, /analytics/offers, /analytics/events, /analytics/insights, /analytics/prediction-trends, /analytics/price-elasticity, /analytics/segmentation, /analytics/inventory, /analytics/confidence-calibration, /analytics/product-performance, /analytics/stale-predictions, /analytics/prediction-drift, /analytics/pricing-lift Advanced: /analytics-advanced/* (revenue, cohorts, funnel, realtime, comparison, attribution, executive-summary). 4.10 WEBHOOKS (/webhooks) — push instead of polling POST /webhooks create (201) body: {"url": "https://...", "events": [...], "secret": ""} url is SSRF-validated (public https endpoints only). GET /webhooks list GET /webhooks/{id} read PUT /webhooks/{id} update {url?, events?, active?} DELETE /webhooks/{id} delete (204) GET /webhooks/{id}/deliveries delivery log (status, response_code, retries) POST /webhooks/retry re-attempt failed deliveries now Valid event names (exact list): event.created, offer.generated, offer.accepted, offer.dismissed, offer.expired, prediction.updated, reward.issued, reward.redeemed Delivery format — PVSN POSTs to your url: Headers: Content-Type: application/json X-PVSN-Event: X-PVSN-Signature: Body: {"event": "", "merchant_id": , "timestamp": "", "data": {...event payload...}} VERIFY EVERY DELIVERY before trusting it (constant-time compare): import hmac, hashlib expected = hmac.new(secret.encode(), raw_body_bytes, hashlib.sha256).hexdigest() ok = hmac.compare_digest(expected, request.headers["X-PVSN-Signature"]) Failed deliveries are retried up to 3 times with exponential backoff. Respond 2xx quickly; do slow work async. 4.11 API KEYS (/api-keys — requires admin; use a Bearer session or an existing key) POST /api-keys create; body {"name": "..."}. Response: {"key": {...metadata...}, "raw_key": "pvsn_live_..."} raw_key appears ONLY here. Store it in a secret manager immediately. GET /api-keys list (metadata + prefix only, never raw) POST /api-keys/{key_id}/rotate revoke + reissue (returns new raw_key once) DELETE /api-keys/{key_id} revoke (204) 4.12 BULK OPERATIONS (/bulk) POST /bulk/products/prices batch price updates [{product_id, price}, ...] POST /bulk/products/stock batch stock updates [{product_id, stock_quantity}, ...] 4.13 COMPETITOR PRICES (/competitors, /products/{id}/competitors) Metered plan resource — see section 3 (PLAN INCLUSIONS) for quota/overage. GET /competitors/provider active provider mode for this merchant GET /competitors/quota {used, limit, remaining, blocked, overage_allowed, overage_rate, message} POST /competitors/refresh refresh the whole catalog (manager+). Enqueues a job; returns {job_id, ...}. GET /competitors/refresh/{job_id} poll a refresh job's status POST /competitors/import bulk CSV-style import (manager+); free, never consumes quota GET /products/{product_id}/competitors list observations for a product POST /products/{product_id}/competitors manual entry (manager+); free POST /products/{product_id}/competitors/refresh refresh ONE product (manager+); counts as one metered product-refresh DELETE /competitors/{observation_id} remove one observation (manager+) 4.14 DISPLAY CHANNELS (/channels) — push live prices to physical/social surfaces GET /channels list configured price channels POST /channels create {channel_type, endpoint_url?, secret?} DELETE /channels/{channel_id} remove a channel POST /channels/sync push current prices to all channels now Digital screens (in-store signage; the screen token is the credential): GET /channels/screens list screens -> [{id, name, token, product_ids, theme, active, last_seen_at, view_path}] POST /channels/screens create {name, product_ids?:[int]|null (null=all), theme?:"dark"} DELETE /channels/screens/{screen_id} delete a screen GET /channels/screen/{token} PUBLIC live price JSON for a screen GET /channels/screen/{token}/view PUBLIC self-refreshing HTML price board Electronic shelf labels (generic HTTP push gateway): GET /channels/esl current ESL config + last push status PUT /channels/esl save {endpoint_url, auth_header_name?, auth_header_value?, active?} DELETE /channels/esl remove ESL config POST /channels/esl/push push all current prices to the gateway now Social price-drop posts: GET/PUT /channels/social/settings {auto_post?, threshold_pct?, channel?, link_base?} GET /channels/social/posts list composed/queued posts POST /channels/social/posts create a post POST /channels/social/compose compose copy for {product_id, old_price?} POST /channels/social/posts/{post_id}/queue queue a post for delivery DELETE /channels/social/posts/{post_id} delete a post 4.15 MENU BOARDS (/menu-boards, public /board/{token}) Restaurant/QSR TV menu boards that render CURRENT dynamic prices. GET /menu-boards list boards POST /menu-boards create (201, manager+) {name, theme?:"dark", active?:true, sections:[{name, items:[{product_id?:int, name?, description?, price?:float, image_url?}]}]} — an item links a product_id (price is computed live) OR is a "free" item that needs both name and price. Max sections/items enforced. GET /menu-boards/{board_id} read one PATCH /menu-boards/{board_id} partial update (manager+) DELETE /menu-boards/{board_id} delete (204, manager+) POST /menu-boards/{board_id}/rotate-token invalidate + reissue the display token (manager+) GET /board/{display_token} PUBLIC — what the TV polls. No auth; the token IS the credential. 404 for unknown/rotated/inactive tokens. Response: {name, theme, generated_at, powered_by:"PVSN", sections:[{name, items:[{key, name, description, image_url, price, base_price, direction:"up"|"down"|"flat", dynamic:bool}]}]} 4.16 AI ASSISTANT + AI PRICING ADVISOR POST /assistant/chat built-in store assistant (paid plans; metered ai_calls) body: {message (str, required), history?:[{role:"user"|"assistant", content}]} => {reply, usage:{method:"ai"|"fallback", ai_calls_used, ai_calls_limit, plan_tier}} 402 on free / out-of-quota plans (surface the upgrade message). POST /ai-pricing/recommend Claude+APE blended price recommendation GET /ai-pricing/strategy strategy analysis for the merchant GET /ai-pricing/explain/{offer_id} per-offer pricing explanation 4.17 TIER-3 ENGINE EXTRAS (/engine) GET /engine/ltv/{customer_id} predicted lifetime value GET /engine/elasticity/{product_id} learned price elasticity for a product GET /engine/bundles/suggestions bundle/kit suggestions from co-purchase GET /engine/forecast-weather upcoming-weather pricing outlook POST /engine/expiry-reminders/send send approaching-need-date reminders (manager+) 4.18 ACCOUNT, BILLING, USAGE & SETTINGS Auth/session (Bearer dashboard sessions; not typical for API-key agents): POST /auth/login => {token, ...}; POST /auth/register; GET /auth/me POST /auth/invite (admin) GET /auth/team (manager+) POST /auth/change-password Merchant settings: GET/PATCH /merchants/me/location store location (drives weather) POST /merchants/me/location/detect auto-detect from IP GET /merchants/me/weather current weather context GET/PATCH /merchants/me/notification-prefs Billing (admin for mutations): GET /billing/plans PUBLIC — array of plan tiers + limits + features GET /billing/subscription | /billing/usage | /billing/invoices POST /billing/checkout | /billing/portal | /billing/change-plan | /billing/cancel Usage: GET /usage rate-limit + plan-usage overview for the credential NPS (in-app survey): GET /nps/eligibility POST /nps {score 0..10, comment?} POST /nps/dismiss 4.19 REAL-TIME AND THE REST OF THE CATALOG GET /sse/events Server-Sent Events stream (auth via X-API-Key or Bearer header; heartbeat keep-alive). Lightweight alternative to polling for offer/prediction updates. GET /sdk/pvsn.js the embeddable JS SDK (public, no auth) GET /health PUBLIC liveness {status, version, environment, uptime_seconds, database, cache}. /health/detailed needs an ops token (not merchant auth). GET /search?q= global search across products/customers/offers POST /cron/pde-refresh force a prediction refresh for this merchant (manager+) POST /cron/auto-approve run auto-approval sweep now (manager+) POST /cron/score-predictions | /cron/segment-customers | /cron/restock-alerts | /cron/expire-offers | /cron/process-subscriptions | /cron/reminders (manager+) These groups follow the same conventions (auth, pagination, errors); read the exact schema for each at https://api.pvsnapp.com/openapi.json: /subscriptions create/pause/resume/cancel; create-from-prediction /rewards balance/tier/issue/redeem; GET /rewards/tiers /segmentation churn-risk, ltv, distribution, refresh /recommendations /customer/{id}, /cross-sell/{product_id} /promotions /bundles /ab-tests /offer-templates CRUD /offer-features batch-generate, schedule, calendar, leaderboard, budget /auto-approval/rules rules CRUD + /auto-approval/batch /notifications/channels CRUD + /notifications/send, /notifications/logs /data/import/{products|customers|events}, /data/export, /data/import/template/{t} /feature-flags GET/PUT (per-flag or bulk) /customer-intelligence/{id}/{360|health|rfm|lifecycle|journey|...} /customer-products/{customer_id} /customers/merge (+ /merge/duplicates) /analytics/* and /analytics-advanced/* (see 4.9) /analytics-extended/{trends/{metric}|cohorts|attribution|basket-analysis| goals|data-quality} /pde/accuracy/{overview|by-cohort|by-product|confidence-calibration} /pricing-analytics/{audit|loss-leaders|conflicts|simulate} /price-alerts, /price-alerts/volatility/{product_id} /support (open a support ticket) ------------------------------------------------------------------ 5. EMBEDDABLE JAVASCRIPT SDK (storefront, browser) ------------------------------------------------------------------ Caution: a key embedded in a public page is visible to anyone. Use a dedicated standard-tier key for storefront SDK use, never an enterprise key. ------------------------------------------------------------------ 6. STEP-BY-STEP WORKFLOWS ------------------------------------------------------------------ WORKFLOW A — first-time integration (do in this order): 1. Verify the key: GET /products?limit=1 (expect 200, not 401) 2. Sync catalog: POST /products for each item. Set unit_cost (margin protection) and lifecycle_days if known. Record the returned id <-> your SKU mapping. 3. Sync customers: POST /customers with external_id = your id. 4. Backfill history: POST /events/batch with past "purchase" events, each with its real `timestamp`. Chunks of <=1000. 5. Set guardrails: POST /pricing-rules (at minimum a global rule with max_discount_percent and, if prices may rise, max_urgency_markup_percent > 0). 6. Register a webhook: POST /webhooks for offer.generated, offer.expired, prediction.updated. 7. Trigger first compute: POST /cron/pde-refresh, then poll GET /predictions until rows appear. WORKFLOW B — daily operation loop: 1. Stream events as they happen (POST /events; batch hourly backlogs). 2. Read predictions: GET /predictions; act on rows whose predicted_need_date is near and confidence >= 0.5. 3. Serve offers: GET /offers?customer_id=X&status=active at render time; show offer.dynamic_price and offer.message; respect expires_at. 4. Close the loop: POST /offers/{id}/accept when redeemed, /dismiss when declined. This feeds elasticity learning — skipping it degrades pricing quality. WORKFLOW C — acting on price recommendations: 1. GET /price-variation/calculate/{product_id} (or POST /price-variation/calculate-batch for the whole catalog). 2. Inspect the adjustment breakdown; sanity-check final_price against your own floor/ceiling expectations. 3. To adopt a recommendation, write it back: PATCH /products/{id} {"price": } or bulk: POST /bulk/products/prices. 4. For "what would the price be if..." questions, use POST /price-variation/simulate — it never persists anything. ------------------------------------------------------------------ 7. HARD RULES — NEVER VIOLATE ------------------------------------------------------------------ 1. NEVER share, print, log, or echo an API key. Not in code samples, not in error messages, not in chat output. Refer to it as pvsn_live_*** only. 2. Respect rate limits: standard 120/min, premium 600/min, enterprise 2000/min. On 429, wait Retry-After seconds; do not hammer. 3. Always send X-Idempotency-Key when retrying a POST/PUT/PATCH. 4. Never invent endpoints. If it is not in this file or in https://api.pvsnapp.com/openapi.json, it does not exist. 5. Offers and predictions are tenant-scoped and engine-generated: do not fabricate offer values; read them from the API. 6. Do not show a dynamic price to a customer after offer.expires_at. 7. Verify the X-PVSN-Signature HMAC on every inbound webhook before acting. 8. event_type must be one of the eight listed values; anything else is a 422. 9. 402 responses mean a plan/trial limit — surface this to the merchant; do not retry around it. 10. Destructive calls (DELETE /products/{id}, DELETE /customers/{id}, key revocation) require explicit human confirmation first. ------------------------------------------------------------------ 8. MCP SERVER (CLAUDE DESKTOP, CLAUDE CODE, MCP-COMPATIBLE AGENTS) ------------------------------------------------------------------ If you are an agent running in an MCP-capable client, you do not need to hand-roll HTTP calls: PVSN ships an official MCP server. IMPORTANT: @pvsn/mcp-server is NOT yet published to npm (npm publish coming soon) — `npx -y @pvsn/mcp-server` will fail with E404 today. Install it from source instead: git clone https://github.com/Reddog30/PVSN.git cd PVSN/sdk/mcp-server npm install && npm run build # builds dist/index.js Package: @pvsn/mcp-server (source: sdk/mcp-server in the PVSN repo; run the built dist/index.js with node; Node >= 18) Transport: stdio Env vars: PVSN_API_KEY (required) — merchant API key, sent as the X-API-Key header (NOT as Authorization: Bearer — see section 2) PVSN_API_URL (optional) — defaults to https://api.pvsnapp.com/v1 Tools exposed (thin typed wrappers over the endpoints in section 4): list_products GET /products get_product GET /products/{id} update_product_price PATCH /products/{id} (writes live price) list_customers GET /customers get_predictions GET /predictions or /predictions/{cid}/{pid} list_offers GET /offers generate_offers POST /offer-features/batch-generate accept_offer POST /offers/{id}/accept get_analytics_overview GET /analytics/overview get_price_recommendation GET /price-variation/calculate/{id} (read-only) log_event POST /events Claude Desktop — add to claude_desktop_config.json (use the absolute path to your local clone's built dist/index.js): { "mcpServers": { "pvsn": { "command": "node", "args": ["/absolute/path/to/PVSN/sdk/mcp-server/dist/index.js"], "env": { "PVSN_API_KEY": "YOUR_PVSN_API_KEY" } } } } Claude Code — one command: claude mcp add pvsn -e PVSN_API_KEY=YOUR_PVSN_API_KEY -- node /absolute/path/to/PVSN/sdk/mcp-server/dist/index.js Once the package is published to npm, these configs will switch to `npx -y @pvsn/mcp-server`. All HARD RULES in section 7 apply unchanged when calling through MCP tools. The recommended adoption loop: get_price_recommendation -> inspect breakdown -> update_product_price with final_price. Support: support@pvsnapp.com | Phone: 720-675-9519 Index file: https://pvsnapp.com/llms.txt