Forms and extras
Custom forms, back-in-stock alerts, order cancellation, product reviews and the blog. Every write on this page is a browser call (rule 5), with no login.
Forms
Merchants build forms in Blanxer (enquiries, bookings, wholesale requests). A custom site renders a published form at /forms/<slug>.
Read a form
GET /public/form/<store_id>/<slug>{
"form": {
"name": "Wholesale enquiry", "slug": "wholesale",
"page_header": "Work with us", "page_subheader": "...",
"title": "...", "description": "...",
"submit_label": "Submit", "success_message": "Thanks! We'll call you.",
"page_footer": "<p>HTML</p>",
"fields": [
{ "key": "full_name", "type": "text", "system": true, "label": "Full Name", "required": true, "width": "half" },
{ "key": "phone", "type": "text", "system": true, "label": "Phone Number", "required": true, "width": "half" },
{ "key": "email", "type": "text", "system": true, "label": "Email Address", "required": false, "width": "full" },
{ "key": "k8f2", "type": "select", "label": "Business type", "required": true, "options": ["Retail", "Online"] }
]
}
}- Only
publishedforms are returned. Anything else is HTTP 400{"message":"Form not found"}; render a 404. - Each field has
key,type,label,placeholder,help_text,required,width(fullorhalf), and for some typesoptions,multiple,sub_fields({key, label, placeholder, icon}),min,max,step. page_footeris HTML; sanitize it before rendering.- Every form has the three system fields
full_name,phoneandemail.
Submit
POST /public/form/<store_id>/<slug>/submit
Content-Type: application/json
{ "data": { "full_name": "Asha Gurung", "phone": "9800000000", "email": "", "k8f2": "Retail" } }The server also accepts the values as a flat body, without data. Send data.
Response: {"success": true, "message": "<success_message or 'Submitted successfully'>"}. Show the message.
Values are keyed by field key and checked against the published form:
| Type | Value | Rule |
|---|---|---|
full_name (system) |
string | 1–200 characters: Full name is required (Required when the key is missing) |
phone (system) |
string | Exactly 10 digits: Phone number must be of 10 digit |
email (system) |
string | Valid email; may be '' unless required (then Provided email address is invalid) |
text, and any type not listed |
string | ≤ 1000 |
textarea |
string | ≤ 5000 |
richtext |
HTML string | ≤ 50000 |
select, radio |
string | One of options |
badges |
string, or string[] when multiple |
From options |
checkbox_group |
string[] | From options |
tags |
string[] | ≤ 30 items, each ≤ 120 |
number, slider |
number | Within min/max (slider defaults to 0–100) |
email |
string | Valid email |
phone |
string | /^\+?[\d\s-]{6,20}$/ |
url |
string | A full URL |
date |
YYYY-MM-DD |
|
time |
HH:MM |
|
datetime |
YYYY-MM-DDTHH:MM |
|
color |
#RRGGBB |
|
checkbox |
boolean | Required means it must be ticked |
multi_text |
{ [sub_field.key]: string } |
Each ≤ 500 |
heading, divider |
— | Layout only; don't send |
- A required custom field that is empty fails with
<label> is required. - Leave out choice fields the shopper didn't answer. An optional
select,radioor singlebadgesfield sent as''fails withInvalid option for <label>. - Errors: HTTP 400
{"error_type":"zod_validation","fields":{"<key>":"<message>"}}. Show each message under its field. - Unknown keys are dropped.
- An unknown or unpublished form: HTTP 400
{"message":"Form not found"}. - Limit: 10 submissions per 15 minutes per IP.
The package's BlanxerForm component renders and submits a form; style it through its class hooks.
Back-in-stock alerts
When a product (or the chosen variant) is out of stock and the store has customer_program.back_in_stock.enabled, show a "Notify me when available" form. Don't show it for coming_soon products.
POST /customer/stock-alert
Content-Type: application/json
{ "store": "<store_id>", "product": "<product_id>", "variant_id": "<variant _id or ''>",
"phone": "9800000000", "email": "asha@example.com",
"product_name": "Pashmina Shawl - Maroon/Large",
"product_url": "https://mystore.com/product/pashmina-shawl" }phoneoremail(or both) is required:A phone number or email is required. Check the phone is 10 digits in the browser.- Leave out a key that is empty.
email: ""fails with HTTP 400Invalid email, even whenphoneis set. product_nameandproduct_urlgo into the alert SMS/email as sent. Send the absolute product URL on the site (https://mystore.com/product/<slug>), without a query string.- Response:
{"data": {"subscribed": true}, "message": "We'll notify you when it's back in stock."}. - If the store has the feature off:
This feature is not enabled for this store.Other errors:Store not found!,Product not found!. - Repeating the same request doesn't create a second alert.
- This is the only
/customer/*route a custom site uses. It needs no login. Everything else under/customeris off limits (rule 7).
The package's StockAlertForm does this.
Cancel an order
On /track/<id>, offer Cancel Order when the order's status is Pending or Processing and the store has checkout.extras.allow_customer_cancellation.
POST /order/public/cancel
{ "store_id": "<store_id>", "order_id": "<order_id>", "phone_number": "9800000000" }- The shopper types the phone number used for the order. Show the placeholder
ending with <last 3 digits>from the masked number in the tracking response. - Success:
{"success": true, "message": "Order cancelled successfully"}. Reload the tracking data. - A phone that isn't 10 characters: HTTP 400
{"error_type":"zod_validation","fields":{"phone_number":"Phone number must be of 10 digit"}}, before any other check. - Wrong phone: HTTP 401
{"fields":{"phone_number":"Provided phone number don't match with order phone number"}}. The phone is checked before the status, so a wrong phone on a Delivered order is also 401. - Other errors (HTTP 400
{message}):Store not found,Order cancellation is not available,Order not found,Only orders in Pending or Processing status can be cancelled. - Cancelling puts the stock back and sends the store's cancellation SMS if it has one.
- Ask "Are you sure you want to cancel this order? This action cannot be undone." first.
Review a product
On /track/<id>, when the order's status is Delivered, offer Review Product for each product in the order that isn't in review.received. (The server doesn't check the status; the page does.)
POST /order/public/submit-review
{ "store_id": "...", "order_id": "...", "product_id": "...",
"rating": 5, "review": "Lovely colour, fast delivery.", "phone_number": "9800000000" }ratingis 1–5; send a whole number (the server doesn't enforce it).reviewis up to 1000 characters.phone_numbermust be 10 characters, else HTTP 400{"error_type":"zod_validation","fields":{"phone_number":"Phone number must be of 10 digit"}}.- Success:
{"success": true, "message": "Review recorded successfully", "rating": 5, "review": "..."}. - Wrong phone: HTTP 401
{"fields":{"phone_number":"..."}}. - Other errors (HTTP 400
{message}):Order item not found(no such order, or another store's),Product has already been reviewed,Provided product not found in the following order,Selected product not found. - The review is published as a verified purchase and counts in the product's
total_rating/review_count.
The package's OrderTracking component includes both the cancel and review flows.
Blog
If the store has published posts, add /blogs and /blog/[slug]. Blanxer's sitemap lists them only when posts exist.
List
GET /public/blog/<store_id>?limit=12&cursor=<next_cursor>&category=<slug>&tag=<tag>&author=<user id>&q=<text>
GET /public/blog/<store_id>/category/<category_slug>
GET /public/blog/<store_id>/tag/<tag>
GET /public/blog/<store_id>/author/<author user id>The category, tag and author routes take the same query parameters as the list (limit, cursor, tag, q, author, category); the path value wins over the query.
{
"items": [{ "_id": "...", "name": "How to care for pashmina", "slug": "pashmina-care",
"excerpt": "...", "featured_image": "https://...", "author": { },
"categories": ["care"], "tags": ["pashmina"], "reading_time": 4,
"view_count": 120, "published_at": "2026-09-01T06:00:00.000Z" }],
"next_cursor": "eyJwIjoi...", "has_more": true,
"all_categories": [{ "slug": "care", "name": "Care", "image": "" }]
}limitis clamped to 1–50, default 12. Passnext_cursorback ascursorfor the next page.qmatches the post name or excerpt (case-insensitive).authoris the author's user id (author.user).all_categorieslists every category with live posts, for filter chips. Blanxer's sitemap lists/blogs?category=<slug>for every blog category incheck_domain.blog_categories, even empty ones, so support the query parameter and show an empty state.
One post
GET /public/blog/<store_id>/<slug>Returns the post with name, slug, excerpt, featured_image, author, categories, tags, reading_time, published_at, seo_title, seo_description, seo_image (already filled from the title, excerpt and image when the merchant left them empty), no_index, sections, plus related (up to 3 posts), and products, single_products and blogs referenced by the post's sections. Unknown or unpublished: HTTP 400 {"message":"Blog post not found"}.
The body is in sections, in Blanxer's section format (the same section codes as the page builder). A custom site renders the text and image sections it understands and skips the rest. Look at a real post's sections for the store you are building before writing the renderer. If no_index is true, add noindex to the page.
Agents read this page at /api-docs/forms-and-extras.md · Edit on GitHub