Design → AI agent → MCP customization
Design your form or landing page in a tool like Canva, then use an AI agent (e.g. Claude) connected to the JomForm MCP to apply that design and further customize it with CSS and JS. This is the fastest way to get a fully branded checkout without touching code.
The flow
1. Design your interface in a design tool
Sketch your form or landing page in any design tool (Canva, Figma, etc.). Export the assets you need — a logo, a hero image, brand colours, and a font. JomForm keeps authority over prices and the payment flow, so you design the look and feel, not the checkout logic.
2. Connect an AI agent to the JomForm MCP
Point any MCP client (Claude, Cursor, etc.) at the server URL https://mcp.jomform.com/mcp. It registers itself via OAuth 2.1 — no API keys needed. The agent can then read and change your workspace through the MCP tools.
3. Upload your media
Use upload_media to upload your design assets (logo, hero image, and code assets like css/js/html/svg/woff2). Images are optimised and served immediately; code assets are scanned before being served.
4. Apply the theme and landing page
Use set_form_theme to apply your brand colours (primary, background, text, card_background), font, logo, layout and spacing to a form. Use product_image_size (small|medium|large|hidden) and product_image_layout (thumbnail-left|image-top) to control the product photo on the form — image-top gives a restaurant menu-card look with the photo above the name. Use set_workspace_landing_page to customise your workspace storefront with a hero, subtitle, body, image, theme colours, logo and call-to-action buttons.
5. Add a restaurant menu (QR ordering)
Use set_menu_page (issue #497: the menu is a PER-FORM setting, so it takes a form_id) to turn a form into a clean restaurant menu. Add Product blocks to the form for the items the menu sells — the menu lists ONLY the products the form references, so unrelated account products can never appear — then set enabled, categories ([{name, product_ids}]), and best_seller_ids (a list of product IDs shown as the Best Seller section). The menu is served at that form's own URL; the workspace home page is never affected and stays your normal landing page / form listing. Use a separate menu form per outlet or menu (lunch vs dinner, two brands) — they coexist independently in one workspace. Pair it with generate_qr to print a per-table QR stand — the ?table=N it encodes is read by the menu page, shown in the header ("Table 8") and carried onto the order as the table number. The menu checks out through the form's own order route: the buyer's name and email are required for CHIP, and the server charges the product's live price. If you want to collect extra info per order, set order_form_slug to a published form — the form's own fields (its builder blocks, or its registered_fields when set) render in the menu checkout step with their labels and required flags, are validated on the server, and are stored on the sale; there is no second field editor, so you create and edit those fields in the normal form builder. Name + email stay mandatory regardless of the attached form, and with no form attached the checkout collects name + email only. The menu is server-rendered and mobile-first: the header shows the table number, cafe name and location with an info panel; a category filter and search let a diner narrow the menu; best-sellers render as two-column square-image cards, and each category as list rows (text left, thumbnail right) with a separator between rows. Items without a photo render cleanly with no empty image slot. All labels follow the workspace default language (en/ms/zh/ta/ar). A diner never has to order a single item: the menu carries a basket (add-to-cart on every item, quantities, a sticky basket bar, checkout) — see the menu-basket section below for the full behaviour.
6. Customize with CSS and JS
For full control, set custom_css (styling/layout) and custom_js (form behaviour) on a form via set_form_theme. Custom JS is owner-gated and scanned for malicious code before it goes live, so it is safe to run on every buyer's page. JomForm keeps authority over prices and payment.
The restaurant menu (QR ordering)
Turn a form into a clean restaurant menu. The menu is a per-form setting, so you configure it on the form itself (Form Settings → Restaurant menu) and it is served at that form's own URL. The workspace home page always stays your normal landing page / form listing, so one workspace can run several menus — two outlets, lunch vs dinner, two brands — simply by using a menu form for each. They are fully independent.
The menu lists only the products the form references through its own product blocks, so unrelated account products can never appear. Pick those into named categories with a searchable product picker and a live preview, and highlight a best-seller. Customers scan a QR stand at the table to order and pay online, and the order reaches you with the table number. Configure it over MCP via set_menu_page /get_menu_page (both take a form_id), or generate the QR stand with generate_qr.
Need more per order — a delivery address, phone number, spice level, notes or pickup time? Attach an optional order form to the menu: the form's own fields render in the checkout step with their labels and required flags, are validated on the server, and are stored on the sale. There is no second field editor — you create and edit those fields in the normal form builder, and the menu settings link straight to it. With no form attached the checkout collects name + email only and still works.
The menu checkout always collects the buyer's name and email (CHIP requires both — they can never be removed), and the server charges each product's live price. The menu is server-rendered and mobile-first: the header shows the table number, cafe name and location with an info panel; a category filter and search let a diner narrow the menu; best-sellers render as two-column square-image cards, and each category as list rows (text left, thumbnail right) with a separator between rows. Items without a photo render cleanly with no empty image slot. All labels follow the workspace default language.
A diner never has to order a single item: the menu carries a basket — see the menu basket for the full behaviour.
Product images on forms
Choose how each product photo appears on your form: size (small 52px, medium 96px, large 160px or hidden) and layout (thumbnail-left, the default, or image-top — a menu-card look with a full-width photo above the name, ideal for a restaurant menu). The renderer serves the matching optimised media variant for the chosen size. Set it in Form Settings → General (with a live preview) or over MCP via set_form_theme (product_image_size / product_image_layout).
Building an order form
Build beautiful order forms with a drag-and-drop builder. Add fields (name, email, phone, textarea, number, select, radio, checkbox, date, time, website, address, consent, hidden, list, repeater, image-choice, captcha), attach products, and publish to your own workspace URL or a custom domain. Forms can be payment forms or response-only — the mode decides what the form collects and what its entries are called, and it is set in the builder.
An agent can build the same form: upsert_form creates or updates it, register_form_fields sets its input fields under a strict contract, and publish_form takes it live.
Publishing a form
Publishing puts your form on the internet at its own URL — the link a buyer opens, or the address you embed. Press Publish in the builder and JomForm confirms it immediately: a panel names the form as live, shows the public URL, and gives you an Open form and a Copy link action, so a first publish never ends in silence. If the URL cannot be resolved yet (for example a workspace whose subdomain is not set up), the panel says so plainly rather than showing a link that goes nowhere.
Two rules decide whether a form may go live. A response-mode form always may — collecting answers is its whole job, and having no products is normal for it. A payment form needs something to sell: publishing one with no products is refused once with a clear message, because such a form can never take money, and you can confirm and publish it anyway if you only meant to collect details. Products can come from a product block, the form's product snapshot, a priced option or shipping choice, or an enabled restaurant menu; adding one any time removes the question.
The same rule covers every way a form goes live — the Publish button, the status control in Form Settings, publishing several forms at once from the Forms list, and AI agents over MCP (publish_form and bulk_forms, both of which accept acknowledge), so an agent is told the same thing you are. A form that is already live is never asked about again.
Custom domains
Put your forms on your own domain (e.g. shop.yourname.com). Add the domain, point a CNAME to custom.jomform.com, and verify. With Cloudflare linked, JomForm can auto-create the CNAME for you. Auto-TLS (Let's Encrypt) uses your account email as the ACME contact by default — you can change it per domain in Settings → Domains.
Form URLs and slugs (rename a form)
Every form is served at the slug you gave it — your-workspace.jomform.com/your-form-slug (or the same path on a verified custom domain). The slug is editable: change it in Form Settings → General and the form moves to the new URL immediately. A slug is 1–40 characters of a-z, 0-9 and dashes (uppercase and spaces are normalised), and it must be free within your workspace — a rename onto a slug another of your forms already serves is refused with an error rather than silently overwriting either form's URL.
Renaming never orphans the URL you already handed out: the old slug keeps working through a temporary redirect (HTTP 301) to the new one, so a printed QR stand, an embedded iframe, a menu's attached order form, and a link already shared in a campaign all keep resolving. The redirect is temporary, not a permanent alias: if another form in the workspace later claims the freed slug, that LIVE form wins and the old URL serves it — a rename's history can never shadow a real form's URL. Slugs cannot be renamed on templates (a template has no public URL) and AI agents do the same thing with the slug parameter on upsert_form, which returns the slug the form now owns.
The menu basket (multi-item ordering)
A restaurant menu is not a one-item form: the menu page carries a full basket so a diner can order a whole table's food in one payment. Here is what the diner experiences and what reaches you. (Nothing to configure — the basket is part of every menu form.)
Tap an item to add it
Every menu card and list row is the add button. Tapping it adds one of that item to the basket — no page load, no navigation to a form — and a small + badge on the item shows how many of it are in the basket. Tapping again adds another, so a diner can build up a multi-item order in one go.
Set quantities in the basket
The sticky basket bar appears across the bottom of the screen as soon as the basket is non-empty. It shows the live item count and running total, and opens the basket panel. Each basket line has a − / + quantity stepper (taking a line to zero removes it), a per-line Remove button, and a Clear basket button for the whole order. The panel also shows the Subtotal and — when a table number is present — the table it applies to. The total shown on screen is display-only: the server re-reads every price from the products table when the order is placed, so the buyer is always charged the live price.
The basket survives filtering, searching and reloads
A diner can narrow the menu by category or search, and the basket is untouched — the same after closing and reopening the page. The basket is stored in the buyer's own browser, keyed to the menu form, so two different menu forms in one workspace (two outlets, lunch vs dinner) keep separate baskets. The basket is cleared once an order is successfully placed.
Checkout: name, email, then CHIP
Checkout is part of the same basket panel: the diner's name and email are both mandatory (CHIP needs a name and an email to create a purchase), plus any extra fields you attached through order_form_slug. Submitting posts the basket to the form's own order route and redirects the diner to the CHIP checkout page to pay. If the order cannot be placed, an error is shown and the basket is kept so the diner can retry — nothing is lost.
A sold-out dish cannot be added, and a stale basket line is dropped
Stock is re-read live, not frozen when the page was served. A menu page stays open while a diner reads it, so a dish can sell out in between: tapping it then adds nothing and tells the diner the dish is sold out (naming it) rather than letting them carry it to checkout. That holds for a dish with variations and for a plain one — a sold-out plain dish is served the same button as an in-stock one, so the tap asks the server before it adds. A basket line whose dish sold out while the basket sat there is dropped when the page is opened again, with a notice naming the dish, so nothing is carried to checkout only to be rejected there. If the live check cannot answer, the page keeps the state it was served with, so the menu still works when the connection does not.
What lands on the sale
The order becomes a normal JomForm sale: one line per basket item (product, quantity, live price), the buyer's name and email, the total, and the extra fields you asked for. A QR order also carries its table number, so the merchant sees which table to serve; and if the diner arrived through an affiliate link (?aff=CODE), the affiliate code is stored on the sale for commission.
MCP tools used
| Tool | Purpose |
|---|---|
| set_form_theme | Apply brand colours, font, logo, layout, spacing, product_image_size, product_image_layout, custom_css and custom_js to a form. |
| set_workspace_landing_page | Customise the workspace storefront: hero, subtitle, body, image, theme colours, logo and CTAs. |
| upload_media | Upload images and code assets (css/js/html/svg/woff2) for the landing page and forms. |
| upsert_form | Create or update a form, including its optional theme. |
| generate_form | Draft a validated form config (blocks + styling) from a natural-language prompt. |
| set_form_confirmations | Set the confirmation message buyers see after submitting. |
| set_menu_page | Turn a form into a restaurant menu (per-form: it takes a form_id) and set its categories, best-sellers and optional order form. |
| get_menu_page | Read a form's restaurant-menu configuration. |
| generate_qr | Generate a QR code (optionally per table) for a published form. |
| create_affiliate | Create an affiliate link (code + name + commission %) for a workspace. Returns the referral URL. |
| list_affiliates | List a workspace's affiliates with their codes, commission and referral URLs. |
| get_affiliate_sales | Read the sales attributed to one affiliate (read-only). |
| set_affiliate_commission | Change an affiliate's commission rate. |
Affiliate marketing (referral links & commission)
An affiliate is a promoter you pay for sales they bring. You give each affiliate a short code, and their link is your storefront URL with ?aff=CODE appended — for example https://your-workspace.jomform.com/?aff=ALI. When a buyer arrives through that link and completes an order, the code is stored on the sale, so the sale is attributed to that affiliate and you can see what they earned. Attribution works on menu orders too (the code is carried to the basket checkout).
Create an affiliate link
In the dashboard, open Settings → Affiliates and add a code, an optional name and a commission percentage. The new affiliate appears in the list with its code, its commission and its ready-to-share referral URL. Over MCP, call create_affiliate with the workspace_id, a code, an optional name / email, and commission_pct (10 = 10%); it returns the affiliate, including the referral_url. A code must be unique within the workspace, and creating or changing an affiliate needs the manage or admin role.
Set the commission
Commission is a percentage stored per affiliate. Change it with set_affiliate_commission (takes affiliate_id and the new commission_pct). The rate used is the one stored on the affiliate, so a change applies to sales attributed from then on; it is not written back onto sales that already happened.
How a sale is attributed
The affiliate shares their referral URL. When a buyer opens it, the form (or menu) page reads ?aff=CODE and carries it into the order it submits, where it is stored on the sale as the affiliate code. No cookie, no redirect chain: the code on the URL the buyer used is what attributes the sale. A buyer with no ?aff= simply produces an unattributed sale.
Read an affiliate's sales
Use list_affiliates to get the affiliate ids, then get_affiliate_sales with the affiliate_id (read-only). Each row gives the sale reference, status, total, currency and date. Attribution is recorded on every sale that came in with the code; the commission percentage is the affiliate's current rate (the figures are returned, not a pre-computed payout ledger).
REST endpoints
The same surface is available over the REST API, authenticated as the workspace:
| Method & path | Purpose |
|---|---|
| GET /v1/workspaces/{id}/affiliates | List the workspace's affiliates. |
| POST /v1/workspaces/{id}/affiliates | Create an affiliate (code, name, email, commission_pct). |
| PUT /v1/workspaces/{id}/affiliates/{affid}/commission | Set the affiliate's commission percentage. |
| GET /v1/workspaces/{id}/affiliates/{affid}/sales | Read the sales attributed to that affiliate. |
Templates (save a form & reuse it)
A template is a saved form you can start new forms from. It is private to your workspace: it never appears in another merchant's gallery and it has no draft/published state to manage. Build one either way:
- Save form as template in the builder, or Save form as template on the Templates page (pick the form) — its fields and design are copied into a template.
- New template on the Templates page — an empty template, ready to fill in.
You do not have to start from a blank form either: the built-in gallery already covers the common shapes — order forms, booking, donation, contact, survey and more — and cloning one copies it into your workspace as a new draft.
Edit a template's fields and design
The Templates page lists your templates with Edit, Use template, Rename and Delete. Edit opens the template in the same builder you use for forms, so you change its fields, blocks and design exactly as you would on a form, then Save. The builder recognises that it is editing a template: there is no slug, no Publish and no Form Settings — a template is not published and has no public URL, so those controls are not shown.
Editing a template does not change forms already created from it. Cloning a template copies its fields and design at that moment, so a form and its template are independent afterwards: fix a typo in a template and the forms you already made keep the old text until you edit them too. Over MCP the same edit is update_template (with template_id from list_own_templates); the REST equivalent is PUT /v1/rest/templates/{id} with title / config / theme (an omitted field is left unchanged).
A template never lands in another merchant's gallery by accident
Your templates and the public gallery are two separate lists, and the builder's template mode keeps them that way: saving a template writes only its fields and design, never a publish. Publishing a template into the shared gallery is a deliberate, separate act that requires a category and a short description line, so a template can never appear in someone else's New-form picker just because you edited it.
That is because every gallery card is rendered from two fields of the template's own config: the category is config.category, and the description is the value of its first Text block — which is why a gallery card always reads as more than a title. A template you write yourself gets the same treatment: set a category and put a short intro line in a Text block, or its card has nothing to show.
Publishing is therefore checked: a template missing either field is refused with a clear message rather than appearing in the gallery as a bare title. Publishing several forms at once behaves differently on purpose — a bulk publish skips such a template while still publishing the other forms you selected, so one unfinished template cannot block a whole batch. The skipped ids are reported back, so the skip is never silent.
Language: dashboard vs. your forms
JomForm has two separate language settings, and they are deliberately not the same one. The supported languages are English, Bahasa Melayu, 中文, தமிழ் and العربية (Arabic also switches the dashboard to right-to-left).
- Dashboard language — the language you see when signed in. It is a per-user setting: change it on Profile (Dashboard language), reached by clicking your account block — your name and email at the bottom of the sidebar, or at the bottom of the menu on a phone — and it follows you into any browser you sign in from, on any machine. It never changes what a buyer sees. Until you pick one, the dashboard uses the language last used in that browser, and English if there is none.
- Form language — the language your customers see on a public form, the menu, the thank-you page and the password gate. It is set per form or per workspace, never per browser and never from your dashboard choice.
Choosing your customers' language
Resolution is form → workspace → English: a per-form language wins when set; otherwise the form uses its workspace's Default form language (Settings → Storefront); otherwise English. Set the workspace default once and every form you publish inherits it, so a Malay storefront does not need the language set on each form. Over MCP the workspace default is set_workspace_settings with default_lang; the per-form language is config.lang on upsert_form. An unsupported code is rejected rather than stored, so a form can never be left on a language with no strings.
The translation covers the buyer-facing chrome — field labels, the total, the submit button, error messages, the menu, and the footer links (Privacy / Terms / Refund) — while your own field labels, product names, prices and descriptions are shown exactly as you typed them. JomForm does not machine-translate your content.
Which language your emails use
Every email JomForm sends follows the same two-axis rule — the language of the recipient, not of whoever triggered it:
- Emails to you and your team (verify your address, sign-in link, password reset, 2FA codes, connecting a social login, a workspace invitation, a feedback reply) use the recipient's dashboard language. Set it on Profile and the same emails arrive in that language.
- Emails to your customers (a payment receipt, the “new sale” and “new response” alerts) use the form language resolved the same way the form itself is: form → workspace → English. A guest who pays without an account still gets the language your form is set to — there is no profile to ask.
A never-edited notification rule is translated too — you only get your own wording back once you have actually edited that rule, and JomForm never rewrites text you wrote. An unset or unsupported language always falls back to English, never to Malay. Arabic emails are sent right-to-left, and the email declares its own language so a mail client or screen reader reads it correctly.
The subject and the body are judged separately, so editing one of them does not pin the other. If you rewrite only the body, your words are kept and the untouched subject still follows the language above; if you rewrite only the subject, the untouched body still follows it. Only the half you actually typed is treated as yours.
Related: the placeholder reference lists every {{token}} you can use in a subject or body.
Your workspace address, and names that are reserved
A workspace slug becomes your public address: a workspace called acme is served at acme.jomform.com, and your forms live under it. A slug is 3–40 characters of a-z, 0-9 and dashes, it cannot end in jomform, and it must not be a name reserved for JomForm.
Reserved names are the addresses JomForm owns or has to protect. They fall into three groups: infrastructure (www, api, app, mail, s3); JomForm's own product and edge addresses that are already routed, such as docs, mcp, templates and custom; and sensitive names that read as JomForm's own business or support channel, such as support, help, status, billing, admin, account, login, payments and checkout.
The reason is impersonation: an address like billing.jomform.com would let a buyer be asked for card or invoice details under JomForm's name, so a workspace cannot hold it even while JomForm is not using it. If you try one, JomForm tells you the name is reserved (not that your slug was malformed) and asks you to pick another. The same rule applies whether you create the workspace in the dashboard, over the REST API (POST /v1/workspaces — the refusal carries the code reserved_subdomain) or through an AI agent with the create_workspace MCP tool.
Renaming is covered by the same rule, because a rename can move your ADDRESS just as a create can set it. Changing the address of an existing workspace — in Settings → Workspace, over the REST API (PATCH /v1/workspaces/{id}) or through the update_workspace MCP tool — is refused with reserved_subdomain if the new slug is a reserved name, and the workspace keeps the address it had.
Image URLs must be https://
Every image/URL you set — the form logo (set_form_theme), the landing-page hero image and logo (set_workspace_landing_page, or Settings → Storefront), product images (create_product / update_product, or the Products editor) and a form's image block — must be an absolute https:// URL. Uploaded media from the media library already qualifies: upload_media returns an absolute https URL on your own media host, which is accepted everywhere.
A value with any other scheme (data:, http:, javascript:, or a relative path) is rejected when you save it, with a clear message, on both the dashboard and the MCP — the same rule on every path. This is deliberate: the page renders the value into an <img src> for your buyers, and a browser will not load an unsafe scheme. Rejecting it at entry means you see the problem immediately instead of getting a silently missing image on the live page.
Leaving an image field empty is always allowed and means “no image” (or, when updating, “keep the current one”).
Example prompt for your agent
"Apply my Canva design to my JomForm checkout: 1. Upload the logo and hero image from my design (upload_media). 2. Set the form theme to my brand colours, font and layout (set_form_theme). 3. Customise my landing page hero and CTA (set_workspace_landing_page). 4. Add custom CSS for spacing and custom JS to auto-focus the first field (set_form_theme)."
JomForm keeps authority over prices and the payment flow — the agent customizes the look and feel, never the checkout logic. See the MCP tool reference for the full list of tools.