Overview
The Manva.ai Storefront API allows third-party developers to build custom storefronts, mobile apps, and integrations on top of any Manva-published site. All endpoints are scoped to a subdomain (the store identifier).
Key points:
- All responses are JSON with
Content-Type: application/json
- Prices are returned as numbers (not in subunits)
- Currency is typically
"INR" but depends on the store's settings
- Rate limiting is enforced per IP on most endpoints
- The
:subdomain parameter must be a lowercase alphanumeric string (hyphens allowed)
Authentication
Most storefront endpoints are public and require no authentication. Endpoints that access customer-specific data (wishlist, orders, loyalty) require a Bearer token obtained from the customer login or signup endpoints.
Authorization: Bearer <customer_token>
Tokens are JWTs valid for 30 days. They are scoped to a single store and customer.
Products
Description
Returns all active products for a published site. Optionally filter by category. Products are sorted by display order, then by newest first. Maximum 100 products returned.
Query Parameters
| Name | Type | Required | Description |
| category | string | optional | Filter products by category name |
Response 200 OK
{
"products": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Organic Cotton T-Shirt",
"slug": "organic-cotton-t-shirt",
"description": "Soft, breathable organic cotton tee.",
"price": 799,
"sale_price": 599,
"currency": "INR",
"images": ["https://res.cloudinary.com/.../tshirt.jpg"],
"category": "Clothing"
}
]
}
Errors
| Status | Body | Reason |
| 404 | {"error":"Site not found"} | Subdomain does not exist or site is inactive |
| 500 | {"error":"Failed to load products"} | Internal server error |
Cart & Checkout
Description
Creates a new order and a Razorpay payment order. Prices are verified server-side (client-side prices are never trusted). Supports discount codes, loyalty point redemption, GST calculation, and shipping fees. If a customer Bearer token is provided, the order is linked to that customer account.
Request Body
{
"items": [
{
"id": "product-uuid",
"qty": 2,
"variant": { "Size": "L", "Color": "Blue" }
}
],
"buyer": {
"name": "Jane Doe",
"phone": "9876543210",
"email": "jane@example.com",
"address": "123 Main St, Mumbai 400001",
"notes": "Leave at door"
},
"discountCode": "SAVE10",
"loyaltyPoints": 100
}
| Field | Type | Required | Description |
| items | array | required | Cart items with product id, qty (1-99), optional variant |
| buyer.name | string | required | Buyer's full name |
| buyer.phone | string | required | Buyer's phone number |
| buyer.email | string | optional | Buyer's email |
| buyer.address | string | optional | Shipping address (max 1000 chars) |
| buyer.notes | string | optional | Order notes (max 500 chars) |
| discountCode | string | optional | Coupon/discount code to apply |
| loyaltyPoints | number | optional | Loyalty points to redeem (requires customer token) |
Response 200 OK
{
"orderId": "uuid",
"orderNo": "ORD1A2B3C",
"razorpayOrderId": "order_abc123",
"amount": 119800,
"currency": "INR",
"keyId": "rzp_live_XXXXXX",
"businessName": "My Store",
"breakdown": {
"subtotal": 1398,
"discount": 100,
"loyaltyDiscount": 50,
"loyaltyPointsUsed": 100,
"loyaltyPointsEarned": 12,
"shipping": 50,
"tax": 0,
"gstInclusive": false,
"total": 1198
}
}
Errors
| Status | Body | Reason |
| 400 | {"error":"Cart is empty"} | No items provided |
| 400 | {"error":"Name and phone required"} | Missing buyer details |
| 400 | {"error":"No valid items in cart"} | All items are invalid or unavailable |
| 404 | {"error":"Site not found"} | Subdomain invalid or inactive |
| 429 | {"error":"Too many checkout attempts"} | Rate limited (10/min per IP) |
| 503 | {"error":"Payments not configured"} | Razorpay not set up for this store |
Rate limit: 10 requests per minute per IP
Description
Verifies the Razorpay payment signature after the customer completes the payment flow. On success, the order status is updated to "confirmed" and "paid". Inventory is deducted, discount usage incremented, and loyalty points are adjusted.
Request Body
{
"razorpay_order_id": "order_abc123",
"razorpay_payment_id": "pay_xyz789",
"razorpay_signature": "hmac_sha256_hex_string"
}
| Field | Type | Required | Description |
| razorpay_order_id | string | required | The Razorpay order ID from the checkout response |
| razorpay_payment_id | string | required | The payment ID from Razorpay checkout callback |
| razorpay_signature | string | required | HMAC-SHA256 signature from Razorpay |
Errors
| Status | Body | Reason |
| 400 | {"error":"Missing fields"} | One or more required fields missing |
| 400 | {"error":"Invalid signature"} | Signature verification failed (order marked as failed) |
| 500 | {"error":"Verification failed"} | Internal server error |
Customer Authentication
Description
Creates a new customer account for the storefront. Returns a JWT token for subsequent authenticated requests. Supports referral codes -- if provided, the referrer earns 50 loyalty points.
Request Body
{
"name": "Jane Doe",
"phone": "9876543210",
"email": "jane@example.com",
"password": "securepass123",
"address": "123 Main St, Mumbai",
"refCode": "REF1A2B3C"
}
| Field | Type | Required | Description |
| name | string | required | Customer's full name (max 255) |
| phone | string | required | Phone number (digits only, max 50) |
| password | string | required | Minimum 6 characters |
| email | string | optional | Email address (max 255) |
| address | string | optional | Shipping address (max 1000) |
| refCode | string | optional | Referral code from another customer |
Response 200 OK
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"customer": {
"name": "Jane Doe",
"phone": "9876543210",
"email": "jane@example.com",
"address": "123 Main St, Mumbai",
"referral_code": "REF4D5E6F"
}
}
Errors
| Status | Body | Reason |
| 400 | {"error":"Name, phone, and password required"} | Missing required fields |
| 400 | {"error":"Password must be 6+ characters"} | Password too short |
| 400 | {"error":"An account with this phone already exists..."} | Duplicate phone number |
| 429 | {"error":"Too many requests"} | Rate limited (20/min per IP) |
Rate limit: 20 requests per minute per IP
Description
Authenticates a customer using their phone number and password. Returns a JWT token valid for 30 days.
Request Body
{
"phone": "9876543210",
"password": "securepass123"
}
Response 200 OK
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"customer": {
"name": "Jane Doe",
"phone": "9876543210",
"email": "jane@example.com",
"address": "123 Main St, Mumbai"
}
}
Errors
| Status | Body | Reason |
| 400 | {"error":"Phone and password required"} | Missing fields |
| 401 | {"error":"Invalid phone or password"} | Wrong credentials |
| 429 | {"error":"Too many requests"} | Rate limited |
Description
Returns the authenticated customer's profile. Requires a valid Bearer token from login or signup.
Response 200 OK
{
"customer": {
"id": "uuid",
"name": "Jane Doe",
"email": "jane@example.com",
"phone": "9876543210",
"address": "123 Main St, Mumbai",
"referral_code": "REF4D5E6F"
}
}
Errors
| Status | Body | Reason |
| 401 | {"error":"Not logged in"} | Missing or invalid Bearer token |
Description
Returns the authenticated customer's order history, sorted by newest first. Maximum 100 orders returned.
Response 200 OK
{
"orders": [
{
"order_no": "ORD1A2B3C",
"items": [{ "name": "T-Shirt", "price": 599, "qty": 1 }],
"subtotal": 599,
"shipping_fee": 50,
"discount_amount": 0,
"total": 649,
"currency": "INR",
"status": "confirmed",
"payment_status": "paid",
"created_at": "2026-04-10T09:15:00.000Z"
}
]
}
Errors
| Status | Body | Reason |
| 401 | {"error":"Not logged in"} | Missing or invalid Bearer token |
| 500 | {"error":"Failed to load orders"} | Internal server error |
Reviews
Description
Returns all approved reviews for a product, sorted by newest first. Includes the review count and average rating. Maximum 100 reviews.
Response 200 OK
{
"reviews": [
{
"id": "uuid",
"reviewer_name": "Jane D.",
"rating": 5,
"title": "Amazing quality!",
"body": "Very soft fabric and great fit.",
"verified": true,
"created_at": "2026-04-08T14:30:00.000Z"
}
],
"count": 12,
"average": 4.3
}
Errors
| Status | Body | Reason |
| 404 | {"error":"Site not found"} | Subdomain invalid or inactive |
| 500 | {"error":"Failed to load reviews"} | Internal server error |
Description
Submits a new review for a product. Reviews require store owner approval before appearing publicly. If a customer Bearer token is provided and the customer has purchased the product, the review is marked as "verified".
Request Body
{
"rating": 5,
"title": "Excellent product!",
"body": "Loved the quality and fast delivery.",
"name": "John"
}
| Field | Type | Required | Description |
| rating | integer | required | 1 to 5 stars |
| body | string | required | Review text (min 5 chars, max 2000) |
| title | string | optional | Review title (max 200) |
| name | string | optional | Reviewer name (ignored if logged in, defaults to "Anonymous") |
Response 200 OK
{
"success": true,
"id": "uuid",
"created_at": "2026-04-13T10:00:00.000Z",
"verified": false
}
Errors
| Status | Body | Reason |
| 400 | {"error":"Rating must be 1-5"} | Invalid rating |
| 400 | {"error":"Review too short"} | Body less than 5 characters |
| 404 | {"error":"Product not found"} | Product ID invalid |
| 429 | {"error":"Too many reviews submitted"} | Rate limited (5/hour per IP) |
Rate limit: 5 reviews per hour per IP
Wishlist
Description
Returns all products in the authenticated customer's wishlist. Maximum 200 items, sorted by newest first.
Response 200 OK
{
"wishlist": [
{
"id": "product-uuid",
"name": "Organic T-Shirt",
"price": 799,
"sale_price": 599,
"currency": "INR",
"images": ["https://..."]
}
]
}
Errors
| Status | Body | Reason |
| 401 | {"error":"Not logged in"} | Missing or invalid Bearer token |
Description
Adds a product to the customer's wishlist. If the product is already in the wishlist, the request succeeds silently (no duplicate created).
Errors
| Status | Body | Reason |
| 401 | {"error":"Login required"} | Missing or invalid Bearer token |
| 404 | {"error":"Site not found"} | Subdomain invalid |
Description
Removes a product from the customer's wishlist.
Errors
| Status | Body | Reason |
| 401 | {"error":"Login required"} | Missing or invalid Bearer token |
Loyalty Points
Description
Returns the customer's loyalty points balance and the store's loyalty program configuration (earn rate, redeem rate, enabled status).
Response 200 OK
{
"enabled": true,
"earn_rate": 5,
"redeem_rate": 1,
"points": 250
}
Errors
| Status | Body | Reason |
| 401 | {"error":"Login required"} | Missing or invalid Bearer token |
| 404 | {"error":"Site not found"} | Subdomain invalid |
Newsletter
Description
Subscribes an email address to the store's newsletter. Duplicate emails are silently ignored (no error).
Request Body
{
"email": "subscriber@example.com",
"name": "Jane",
"source": "popup"
}
| Field | Type | Required | Description |
| email | string | required | Valid email address |
| name | string | optional | Subscriber name (max 100) |
| source | string | optional | Subscription source e.g. "popup", "footer" (max 50, defaults to "popup") |
Errors
| Status | Body | Reason |
| 400 | {"error":"Valid email required"} | Missing or invalid email |
| 404 | {"error":"Site not found"} | Subdomain invalid |
| 429 | Rate limit response | 10 requests/min per IP |
Rate limit: 10 requests per minute per IP
Order Tracking
Description
Looks up an order by its order number and the buyer's phone number (last 4 digits matched for privacy). No authentication required -- useful for guest buyers.
Request Body
{
"orderNo": "ORD1A2B3C",
"phone": "9876543210"
}
| Field | Type | Required | Description |
| orderNo | string | required | The order number (e.g. "ORD1A2B3C") |
| phone | string | required | Buyer's phone (last 4 digits used for matching) |
Response 200 OK
{
"order": {
"order_no": "ORD1A2B3C",
"buyer_name": "Jane Doe",
"items": [...],
"subtotal": 1198,
"shipping_fee": 50,
"discount_amount": 100,
"total": 1148,
"currency": "INR",
"status": "shipped",
"payment_status": "paid",
"created_at": "2026-04-10T09:15:00.000Z",
"updated_at": "2026-04-11T14:30:00.000Z"
},
"businessName": "My Store"
}
Errors
| Status | Body | Reason |
| 400 | {"error":"Order number and phone required"} | Missing fields |
| 404 | {"error":"Order not found. Check your order number and phone."} | No match found |
| 429 | {"error":"Too many lookups"} | Rate limited (20/min per IP) |
Rate limit: 20 requests per minute per IP
Contact / Lead Form
Description
Submits a contact form or lead capture from a published site. Accepts both JSON and URL-encoded form data. The site owner is notified asynchronously. Maximum 10,000 leads per site.
Request Body
{
"name": "Jane Doe",
"email": "jane@example.com",
"phone": "9876543210",
"message": "I am interested in your services."
}
| Field | Type | Required | Description |
| name | string | optional | Contact name (max 255) |
| email | string | optional | Email address (validated if provided) |
| phone | string | optional | Phone number (max 50) |
| message | string | optional | Message body (max 2000) |
Response 200 OK
{
"success": true,
"message": "Thank you! We will get back to you soon."
}
Note: If the request does not include Accept: application/json, an HTML thank-you page is returned instead.
Errors
| Status | Body | Reason |
| 400 | {"error":"Invalid email address."} | Email format validation failed |
| 404 | {"error":"Site not found."} | Subdomain invalid or inactive |
| 429 | {"error":"Too many submissions..."} | Rate limited (10 per 15 min per IP) |
| 429 | {"error":"This site has reached its lead storage limit."} | 10,000 leads cap reached |
Rate limit: 10 submissions per 15 minutes per IP
Password-Protected Pages
Description
Verifies a password for a protected page and returns a time-limited access token (valid 24 hours). The token should be stored client-side and sent in subsequent page requests.
Request Body
{
"password": "secret123"
}
| Field | Type | Required | Description |
| password | string | required | The page password set by the store owner |
Response 200 OK
{
"success": true,
"access_token": "hex_random_token_string"
}
Errors
| Status | Body | Reason |
| 400 | {"error":"Password required"} | No password provided |
| 401 | {"error":"Incorrect password"} | Wrong password |
| 404 | {"error":"Site not found or not protected"} | Site does not exist or has no password set |
| 429 | {"error":"Too many attempts. Wait a minute."} | Rate limited (10/min per IP) |
Rate limit: 10 attempts per minute per IP
Manva.ai Storefront API Documentation · Built with Manva.ai
All endpoints are versioned at /api/sites/:subdomain/