Category: Tutorials

  • Generate PDF Invoices, Tickets and Confirmations from a WordPress Form

    Generate PDF Invoices, Tickets and Confirmations from a WordPress Form

    Somebody books a slot, buys a ticket, or requests a quote. The follow-up they expect is a document: an invoice they can forward to accounting, a ticket they can show at the door, or a confirmation with the details in writing.

    In most WordPress form plugins that means a second plugin. Fluent Forms ships it as a separate Fluent PDF Generator plugin. Gravity Forms users install Gravity PDF. Formidable sells it as an add-on. The form collects the data, and something else turns it into a PDF.

    CraftForms has the PDF builder in the core plugin, and it’s in the free version. You design the document in the same block editor you use for pages. Merge tags pull in the submission data. A QR or barcode block draws a code that’s unique to each document. You then choose where the file goes: the confirmation email, the saved submission, or (in Pro) the order.

    This guide builds an event ticket with a scan-in QR code, then covers invoices and the order side.

    Diagram attachment points

    The model: PDFs attach to something

    The design choice that matters most: there is no standalone “Generate PDF” step in the form’s submit actions. An earlier version had one, and it confused people. If nothing emailed or stored the file, it effectively went nowhere.

    So a PDF now attaches to something that owns it. Three places have an Attach PDF(s) setting:

    Attachment pointWhere the file ends upPlan
    Save Submission actionDocuments section of the submission’s detail view, with download linksFree
    Send Email (Plain Text) / Send Email (Template) actionAttached to that emailFree
    Catalog itemAttach PDF(s) panelDocuments section of the order created from the formPro

    Email actions also have an Attach files picker for static Media Library files, like terms and conditions or a venue map, next to the generated PDFs.

    If the same template is picked in two places, it’s rendered once. The first action that needs it generates the file, and later actions reuse it. The customer’s email attachment and the admin’s download link are the same document.

    Step 1: Create a PDF template

    Go to CraftForms → PDF Templates → Add New. A starter picker opens with three designs:

    • Basic Invoice: A4 portrait
    • Event Ticket: A5 landscape, with a QR code
    • Shipping Label: 4×6 in, with a Code 128 barcode

    You can also Start from scratch or Import from file (templates export and import like forms). Pick Event Ticket.

    This is the regular block editor with a few PDF-specific settings. The Page panel in the sidebar sets:

    • Page size: A3, A4, A5, Letter, Legal, or Custom (width and height in mm)
    • Orientation: Portrait or Landscape
    • Margins (mm): top, right, bottom and left

    The canvas is sized to the page, and a red guide line shows where each page break falls. That line is useful: if your invoice spills onto a second page, you see it while editing.

    Craftforms pdf ticket

    Step 2: Fill it with submission data

    Text in the template takes merge tags in double curly braces. The Dynamic Data panel lists them, so you don’t have to memorise names. The ones you’ll use most:

    TagResolves to
    {{email.<field>}}A form field value, e.g. {{email.name}}, {{email.email}}, {{email.address}}
    {{all_fields.table}}Every submitted field as a ready-made table (good for “copy of your submission” PDFs)
    {{site.name}}, {{site.url}}Your site
    {{date.date}}, {{date.time}}When the submission happened
    {{pdf.number}}The document number (see below)
    {{pdf.generated_at}}Render timestamp
    {{order.id}}, {{order.total}}, {{order.currency}}, {{order.token}}The order the form created (Pro, catalog/checkout forms)

    email. is the prefix for form fields in every CraftForms template (emails and PDFs), so a tag that works in your confirmation email works in the PDF too.

    About {{pdf.number}}. When the PDF is attached through Save Submission, it’s the saved submission’s ID. When the form created an order (Pro), it’s the order ID. That gives every document a unique number with no extra setup.

    Step 3: Add a QR code that’s unique per ticket

    The Event Ticket starter already has a QR Code / Barcode block. Select it and look at its settings:

    • Type: QR Code, Code 128, EAN-13 or UPC-A
    • Value: can be a merge tag
    • Error-correction level (for QR codes), foreground/background colour, and size

    The value is resolved per document, on the server, before the code is drawn. Set it to a merge tag and each ticket gets its own code. The editor shows a sample preview, and the real code is generated when the PDF is rendered.

    What to encode:

    • Free, Save Submission path: {{pdf.number}}. That’s the submission ID. At the door, look it up under Submissions.
    • Pro, catalog or checkout form: {{order.token}}. That’s the order’s unique token, which is what the starter uses. A token is harder to guess than a sequential ID, which matters if the QR code is the thing that gets someone in.

    A value the symbology can’t encode (a 12-digit string in an EAN-13 block, or a tag that resolved to nothing) doesn’t break the document. The PDF still generates; the code is just left out or replaced with a placeholder.

    Publish the template. (Drafts also appear in the attach pickers, so you can test before going live.)

    Sample event ticket

    The Event Ticket starter, rendered by the real generator with sample data. The font is embedded automatically.

    Step 4: Attach it to the form

    Open your registration form and go to its submit actions.

    1. Save Submission: open its settings, find Attach PDF(s), and tick Event Ticket.
    2. Send Email (Template) (or Plain Text) to the attendee: tick the same Event Ticket under Attach PDF(s). Optionally add a venue map under Attach files.
    3. Optionally, a second email to yourself with the same attachment.

    Order matters here. Put Save Submission above the emails. The file is generated once by whichever action gets to it first. If Save Submission runs first, {{pdf.number}} has a value and every copy (email attachment and admin download) shows the ticket number. If an email runs first, there’s no submission ID yet, and the shared file has an empty number.

    Craftforms pdf save submission attachment setting

    Submit the form once. The attendee gets an email with Event Ticket.pdf attached. In Submissions, the entry’s detail view has a Documents section with the same file.

    Craftforms pdf submission attachment

    What happens when it renders

    Diagram render pipeline

    Two things set this apart from “print the page to PDF”:

    It’s all server-side. No visitor browser is involved after you save the template. That’s what lets it work for triggers without a browser. The renderer is Dompdf, bundled with the plugin, so there’s nothing to install on the server and no headless Chrome to maintain.

    The compiler knows what Dompdf can’t do. Server-side PDF engines don’t support flexbox or grid, and block-editor columns are built on flexbox. When you save a template, CraftForms converts columns to tables and inlines the styles, including fixed-width columns next to flexible ones (the classic “logo left, invoice details right” header). The fonts your theme uses are embedded automatically, from the theme’s font files or Google Fonts, so the PDF doesn’t fall back to a generic typeface.

    Files aren’t public. Generated PDFs are stored in a protected folder, not the Media Library. Invoices contain names, addresses and prices, so each download link carries a per-file token rather than a guessable URL.

    Invoices

    The Basic Invoice starter works the same way: A4, your site name at the top, customer details from {{email.*}}, {{order.total}} and {{order.currency}} for the amount, and {{pdf.number}} as the reference.

    For a quote or order form without the Pro order system, attach it through Save Submission and point the totals at your form’s price fields instead of order.*. The number is the submission ID.

    For catalog and checkout forms (Pro), open the catalog item and use its Attach PDF(s) panel. The selected templates are generated when the order is created and linked to it. They appear in the Documents section of the order’s detail view, and {{order.*}} tags are filled in. This panel only lists published templates.

    What’s free and what’s Pro

    FreePro
    PDF Templates builder, starters, QR/barcode block
    Attach to Send Email actions
    Attach to Save Submission (Documents in submission detail)
    Custom page sizes, auto font embedding, protected storage
    Catalog item → attach to order (Documents in order detail)
    {{order.*}} tags (orders are a Pro feature)

    Building and generating PDFs is free. Linking documents to orders is Pro, because orders are Pro.

    Limits worth knowing

    • Designed for short documents. Tickets, invoices, labels and confirmations fit on one or two pages. On multi-page documents, page margins aren’t repeated at intermediate page breaks.
    • No flexbox or grid effects beyond columns. Columns, groups, images, tables and text all translate. Layout tricks that only work with flexbox or grid won’t.
    • Template changes apply on re-save. The compile step runs when you save the template, so edit, then Update.
    • Not a signature tool. There’s no signature field yet. It’s on the roadmap, and when it ships, the captured signature will be usable in PDF templates.

    Try it

    1. CraftForms → PDF Templates → Add New → pick Event Ticket.
    2. Set the QR block’s value to {{pdf.number}} and publish.
    3. On your form: Save SubmissionAttach PDF(s) → Event Ticket.
    4. Below it: Send Email (Template)Attach PDF(s) → Event Ticket.
    5. Submit once. Check your inbox and the submission’s Documents section.

    That’s the whole setup, with no second plugin.


    Related: Email templates + testing with Mailtrap


  • How to Accept Appointments Online for a Salon, Spa, or Massage Studio (2026)

    How to Accept Appointments Online for a Salon, Spa, or Massage Studio (2026)


    TL;DR

    • A salon booking form needs three things a contact form can’t do: show only the times you’re actually free, stop two clients grabbing the same 2 p.m. slot, and take a deposit that depends on which treatment was chosen.
    • CraftForms splits that across two pieces. The Booking Datepicker Field in Single Date (Optional timeslots) mode draws the calendar. A Catalog item in Single Date With Time Slots mode holds your real hours, slot length, buffers and capacity — and it’s the only thing that checks availability.
    • The connection between them is made on the page where the form is embedded, not inside the form editor. That trips people up, so it gets its own section.
    • One Catalog item = one bookable resource (a chair, a room, a therapist). Not one per service. Services live in the form as a choice field; each option carries its own price.
    • The deposit is just a price formula. A Linked smart variable reads the selected option’s price, and the formula service_price * 0.3 charges 30% of it — recalculated on the server at submit, so the browser can’t change what’s charged.
    • Everything lands in CraftForms → Orders, and the slot is held the moment the form is submitted — before the card even clears.

    If clients still book you by text, phone, or DM, you’re the bottleneck. Every appointment means a back-and-forth: “Are you free Thursday?”, checking your book, replying, waiting for confirmation, and then hoping the client doesn’t ghost you after all that. Multiply that by every new booking, every reschedule, and every “just checking my slot is still there” message, and you’re spending hours a week doing something a form should do in seconds.

    The fix most people reach for is a dedicated booking app — Calendly, Fresha, Acuity. They work, but they live on someone else’s domain, look nothing like your brand, and usually charge you monthly whether you use them or not. If you’re already running WordPress, you can put the exact same booking experience — pick a date, pick a time, pay a deposit, get a confirmation — directly on your own site, styled the way you want it.

    This guide builds that from scratch with CraftForms.

    What You’ll Need

    • WordPress with CraftForms Pro installed — the booking datepicker field, the Catalog (availability tracking), Payments and Orders are all Pro features
    • A Stripe (or PayPal) account — free to create at stripe.com, only takes a per-transaction fee
    • About 30 minutes. You build one form and one Catalog item per bookable resource — adding a fifth treatment to the menu later is a one-line change to a dropdown, not a new setup
    Craftforms appointment form

    Step 1 — Why Calendly or Fresha Often Isn’t the Right Fit

    Third-party booking apps solve availability and double-booking, but they come with trade-offs that matter for a salon or spa:

    • It’s not your brand. The client clicks “Book Now” and lands on a generic scheduling page with someone else’s logo, someone else’s colours, and often someone else’s ads.
    • It’s a recurring cost regardless of volume. Most of these tools charge a monthly fee per staff member or location, whether you had two bookings or two hundred that month.
    • Payment and booking are two different systems. Taking a deposit usually means bolting on yet another integration, or sending clients to a separate checkout page.
    • Your data lives somewhere else. Client names, appointment history, and payment records sit in a third-party dashboard instead of your own WordPress database.

    Building the booking flow inside CraftForms keeps everything — the form, the calendar, the payment, and the client record — on your own site, styled to match the rest of your business.


    Step 2 — Build the Form

    Go to CraftForms → Forms, click Add New, and name it — e.g. “Salon & Spa Appointment.”

    The client’s details

    Add a Text Input Field for the name, a second one with Type: Email for the email address, and optionally a third with Type: Tel for a mobile number. Nothing unusual here.

    The treatment menu, with prices attached

    Add a Select Field (or a Radio Field if you’d rather show the options laid out) named service. In its Options editor, give each option a label, a value, and — this is the part that matters — a price:

    LabelValuePrice
    Haircut & style — $45haircut45
    Cut, colour & highlights — $95colour95
    60-minute deep-tissue massage — $70massage70
    Signature facial — $65facial65

    That per-option price is what the deposit formula reads in Step 4. You never type prices twice.

    The date & time picker

    Insert the Booking Datepicker Field block. In Form Field Type, choose Single Date (Optional timeslots) — the client picks one calendar date, then one time on that date. (The other modes are for date ranges: Dates Range for hotel-style check-in/check-out, Continuous Duration for meeting rooms, Seasonal for rentals whose slot times change by month.)

    Name the field something you’ll recognise in templates — appointment works. The field’s help text tells you what that name gets you: {{field.appointment.date}} and {{field.appointment.time}}.

    The Time Slots panel on this field is a fallback, not your schedule. Once you connect a Catalog item in Step 3, the item’s Business Hours, slot interval, notice period and real availability take over completely, and the field’s own copies are ignored. Set your hours there, once — don’t try to keep two copies in sync.

    Finish with a Textarea Field for notes, an InfoBlock showing the running total, and a Submit Button — label it something that reflects what happens next, like “Book & pay deposit.”


    Step 3 — Prevent Double-Bookings with a Catalog Item

    This is the piece that stops two clients from booking the same 2 p.m. slot. Go to CraftForms → Catalog and add a new item.

    One item per resource — not per service

    Name it after the thing that can only be in one place at a time: “Serenity Spa — Treatment Room 1”, or the therapist’s name if you’re a one-person studio.

    It’s tempting to create one Catalog item per service — “Haircut”, “Massage”, “Facial” — but that quietly breaks the thing you came here for. Each item tracks its own availability, so a 2 p.m. haircut booking wouldn’t block the 2 p.m. massage slot, and you’d end up double-booked on the same chair. Services belong in the form’s dropdown; the Catalog item is the chair.

    If you have three chairs and any of them will do, that’s not three items either — it’s one item with Occupancy Model: Shared (multiple practitioners) and Capacity: 3. Use separate items only when clients genuinely book a specific person or room.

    The Booking settings

    • Type: Booking (this can’t be changed after the item is created, so get it right first time)
    • Booking Type: Single Date With Time Slots (Appointments / Rentals)
    • Availability Window — From Dynamic / 0 days from today, Until Dynamic / 60 days. This is how far ahead clients can book; without it the calendar stretches pointlessly into next decade
    • Business Hours — tick the days you’re open and set opening and closing times per day. In this mode each day also gets an optional Break — set it to 13:0014:00 and no slot will be offered that overlaps lunch
    Craftforms catalog item booking appointment form

    Scheduling: interval and buffers

    This is the part worth reading slowly, because the names are easy to misread.

    • Slot interval (min) — the length of one appointment, not the gap between start times. Set 60 for hour-long treatments.
    • Buffer before / Buffer after (min) — cleanup or setup time reserved around each appointment. These widen the grid: start times land every interval + before + after minutes. With a 60-minute interval and a 15-minute buffer after, a 09:00 open produces 09:00, 10:15, 11:30, … — each client gets their hour, and you get a quarter of an hour to reset the room.
    • Notice (min) — the minimum lead time before a slot can be booked. 1440 requires 24 hours’ notice; 0 lets someone book the next slot going.

    Custom Availability, just below, is where one-off exceptions go — a public holiday you’re closed, or a Sunday you’re opening specially. It overrides the weekly pattern for named dates without you touching Business Hours.

    Tell it which field to watch

    Scroll to Date Field Variable and type the name of your datepicker field — appointment. On a form with a single datepicker you can leave this blank and it will match anything, but naming it is a good habit and it’s required the moment a form has two.

    Craftforms catalog item booking appointment form datepicker connect

    Step 3b — Connect the Catalog Item to the Form

    Here’s the step that isn’t where people look for it: the connection is made on the page where the form is embedded, not in the form editor.

    When you save a CraftForms form, CraftForms generates a synced pattern for it. You place the form on a page by inserting that pattern — never by copying the form’s blocks into the page. Then:

    1. Edit the page and click the inserted form (the synced pattern block).
    2. Either use the Catalog Item button in the block toolbar, or open the document sidebar and find the CraftForms Catalog panel.
    3. Pick your item from Linked Catalog Item, and update the page.
    Craftforms catalog item booking appointment form connect synced pattern

    Because the link lives per placement, the same form can appear on two pages linked to two different rooms. That’s the whole reason it’s wired this way.

    Once connected, the calendar stops being decorative. It calls your site for real availability, greys out days with nothing free, and lists only the open times for the day the client picks.

    What happens when someone books

    The slot is held at submission — not when the payment clears. CraftForms creates the order, atomically reserves the date-and-time row against the item’s capacity, and only then captures the payment. If two people hit submit on the same slot within the same second, exactly one reservation succeeds; the loser’s order is cancelled and no money moves. You never maintain an availability calendar by hand.


    Step 4 — Charge a Deposit with Stripe

    CraftForms has no separate “deposit” toggle, and doesn’t need one. A deposit is a price formula that returns a fraction of the treatment price.

    Read the selected treatment’s price

    Open Smart Variables in the form’s document sidebar and add a variable:

    • Type: Linked
    • Variable name: service_price
    • Linked field: service (your treatment dropdown)
    • Lookup value: Price
    • Default value: 0
    • Expose as field variable: on, so you can print it in an InfoBlock

    A Linked variable builds its lookup table from the field’s own options at render time — pick a different treatment and service_price follows, with no table for you to maintain.

    Write the formula

    In the Pricing panel, click Manage Price Formula:

    service_price * 0.3
    

    Swap 0.3 for 0.5 for a half-deposit, or use service_price on its own to take the full amount up front. No round() needed — CraftForms rounds the price result for you. Whatever you write is recalculated on the server at submission — the number the browser sends is checked, never trusted.

    You can show both figures live in your InfoBlock:

    Treatment price: {{form.currency}}{{field.service_price}} · 30% deposit due now
    Deposit today: {{form.currency}}{{form.price}}
    

    Set your currency

    CraftForms → Settings → Ecommerce holds the Default currency and the Number format (1,234.56 vs 1.234,56 and friends). These are site-wide, not per form.

    Add a payment connection

    CraftForms → Settings → Payments has two parts.

    Redirect URLs at the top: a Payment success URL (a page you create with the Order Summary block on it) and a Payment cancel URL (usually the booking page itself).

    Payment Connections below it: click Add Payment Connection and fill in

    • Name — e.g. “Stripe — Salon (Test)”. Connections are named because you can have several
    • Gateway — Stripe (can’t be changed after creation)
    • Mode — Test or Live. One connection carries one set of keys, so make a separate connection for your live keys rather than editing this one later
    • Publishable key (pk_…) and Secret key (sk_…)
    • Webhook — copy the Webhook URL shown here into your Stripe Dashboard under Developers → Webhooks, then paste the Webhook signing secret (whsec_…) back in. This is what flips an order to paid once Stripe confirms the charge

    We have a video tutorial about Stripe payment integration & configuration. It shows exactly how to connect Stripe account. Since version 1.8 the UI is slightly changed in CraftForms, but the principle remains.

    Add the Payment block

    Back in the form, drop a CraftForms Payment block in above the submit button. In its sidebar:

    • Payment Gateways — choose your Stripe connection. (Leave both dropdowns on “None” and the block won’t render at all on the front end.)
    • Payment ModeHosted Checkout redirects to Stripe’s page and back to your success URL; Embedded Payment puts the card fields inline on the form.
    • Customer Data — map your email and name fields so Stripe gets a real customer identity and can send its own receipt.

    There’s no “create an order” action to add. When a Catalog item is linked, order creation happens automatically, before your other submit actions run.


    Step 5 — Send a Confirmation Email

    The Stripe receipt says what was charged. It doesn’t say when to turn up. Add a Send Email action in Submit Actions:

    • Send To: {{email.email}}
    • Subject: Your appointment at {{site.name}} is booked
    • Body:
    Hi {{email.client_name}},
    
    Your appointment is confirmed.
    
    Treatment: {{email.service}}
    Date: {{email.appointment.date}}
    Time: {{email.appointment.time}}
    
    Deposit paid: {{email.currency}}{{email.price}}
    The balance is due in the salon.
    
    Need to move it? Just reply to this email.
    
    — {{site.name}}
    

    The tags worth knowing:

    TagGives you
    {{email.<field>}}The field’s display value — a choice field returns the option label, not its slug
    {{email.appointment.date}} / .timeThe two halves of the booking field, separately
    {{email.price}}The charged amount, formatted to your Number format setting
    {{email.currency}}The currency symbol
    {{site.name}}, {{date.now}}Site and timestamp context

    Then tick Attach booking calendar invite (.ics) on the action. The client gets a one-click “add to calendar” file with their appointment in it — the single highest-value box on that screen for a business that lives on people showing up.

    For a branded HTML email — logo, colours, layout — build one under CraftForms → Email Templates and use the Send Email (Template) action instead, which carries the same .ics option.

    If you want your whole schedule in your own calendar app, the Catalog item’s iCal Import / Export section gives you a subscribable export feed — and an import field that blocks dates pulled from another platform’s calendar, if you’re still winding one down. There’s a fuller walkthrough in our guide on syncing CraftForms bookings to Google Calendar.


    Step 6 — Managing Appointments from the WordPress Admin

    Every booking appears in CraftForms → Orders with the client, the treatment, the appointment date and time, the amount, and the payment status. Opening one shows every submitted field and the Stripe payment reference.

    Craftforms orders booking

    Statuses run pending → confirmed → paid → refunded. A deposit order simply shows the deposit as its total; collecting the balance in the chair is your own process, not something the order tries to model.

    The raw submissions are kept separately under CraftForms → Submissions, so you keep the enquiry record even for bookings that never got paid.


    What Else Can You Build With This?

    The same mechanism — a Catalog item, Business Hours, slot interval, buffers, advance notice — covers anything where a customer picks one date and one time: tutoring sessions, consultations, test drives, guided tours, photo-studio sittings. Only the pricing changes shape. A tour operator on this exact setup prices per head with a formula as short as participants * 15, and caps the party size against the slot’s remaining capacity.

    If your customers pick a duration rather than a fixed slot — a meeting room booked for 90 minutes starting whenever — that’s the Continuous Duration booking type instead, and it deserves its own walkthrough.


    Summary

    1. A form with a Select Field carrying per-option prices, and a Booking Datepicker Field in Single Date (Optional timeslots) mode
    2. A Catalog item per bookable resource — hours with breaks, slot length, buffers, notice period, availability window, capacity
    3. The two connected on the page, via the synced pattern block’s Catalog Item control
    4. A Linked smart variable reading the chosen treatment’s price, and service_price * 0.3 turning it into a deposit — recalculated server-side
    5. A named Stripe Payment Connection plus the Payment block, with the slot held at submission and released automatically if the charge fails
    6. A confirmation email carrying the date, the time, the amount, and an .ics invite
    7. Every booking in Orders, with your own schedule available as an iCal feed

    No per-seat monthly fee, no client leaving your site to pay someone else, and the whole client record in your own database.


  • How to Use MCP to Build and Fill WordPress Forms (2026 Guide)

    How to Use MCP to Build and Fill WordPress Forms (2026 Guide)

    Most “AI form builder” features shipping in 2026 work the same way: you paste an API key into a settings screen, type a sentence into a textbox, and the plugin’s own server round-trips your prompt to a model and hands back a form. It works, sometimes. But the AI is a feature inside the plugin. It can’t be swapped, it can’t be scripted, and it can only do what the vendor exposed in that one textbox.

    WordPress 6.9 opened a different path. It shipped the Abilities API — a core registry where any plugin can declare typed, permission-checked operations. And in February 2026 the WordPress AI team shipped the MCP Adapter, which takes those registered abilities and exposes them as Model Context Protocol tools that any MCP client can discover and call.

    Put those two together and the picture inverts. Instead of an AI feature living inside your form plugin, your form plugin publishes capabilities that any agent — Claude, Cursor, a CI script, your own code — can pick up and use. No vendor key. No textbox.

    This guide walks the whole thing end to end: install the adapter, register a server, connect a client, and have an agent build a working form. Then the other half nobody covers — having an agent fill one.

    What each piece actually is

    Three layers, and it’s worth being precise about which does what, because the names get used interchangeably and they shouldn’t be.

    The Abilities API is WordPress core (6.9+). A plugin calls wp_register_ability() with a name, a JSON Schema for its input and output, a permission callback, and an execute callback. That’s it. It’s a registry — it knows nothing about AI.

    The MCP Adapter is a separate plugin from the WordPress project. It reads the abilities registry and republishes abilities as MCP tools over a real MCP transport. It is the bridge, and it’s the piece people mean when they say “WordPress has MCP now.”

    Your form plugin registers the abilities. CraftForms registers two:

    AbilityWhat it does
    craftforms/create-formCreates a form post with optional price formula, transformations, and submit actions
    craftforms/add-fieldAppends a field to an existing form as correctly structured Gutenberg blocks

    Both ship in CraftForms free. There’s no AI tier and no key to buy — the abilities register on any install running WordPress 6.9 or later, and are simply absent on older versions (the registration hooks never fire).

    The distinction that matters: CraftForms doesn’t talk to a model. It declares what it can do, in a schema any model can read. Which model you point at it is entirely your business.

    Step 1 — Install the MCP Adapter

    The adapter isn’t on the plugin repository yet; it’s distributed from its GitHub releases.

    wp plugin install https://github.com/WordPress/mcp-adapter/releases/latest/download/mcp-adapter.zip --activate
    

    Requirements are WordPress 6.9+ and PHP 7.4+. Activating it gives you a default server and a WP-CLI command. Check what it found:

    wp mcp-adapter list
    
    ID                            Name                        Version  Tools  Resources  Prompts
    mcp-adapter-default-server    MCP Adapter Default Server  v1.0.0   3      0          0
    

    Three tools — those are core’s own abilities (site info, user info, environment info). CraftForms’ abilities are registered, but the default server doesn’t expose them. That’s deliberate: the adapter makes you opt in to what an agent can reach.

    Step 2 — Register a server that exposes the form abilities

    Drop this in an mu-plugin (wp-content/mu-plugins/craftforms-mcp-server.php):

    <?php
    /**
     * Plugin Name: CraftForms MCP Server
     * Description: Exposes the CraftForms abilities as MCP tools via the WordPress MCP Adapter.
     */
    
    add_action( 'mcp_adapter_init', function ( $adapter ) {
        $adapter->create_server(
            'craftforms',                       // server id
            'craftforms-mcp',                   // REST namespace
            'mcp',                              // REST route
            'CraftForms MCP Server',            // name
            'Build and configure CraftForms forms from an AI agent.',
            '1.0.0',
            [ \WP\MCP\Transport\HttpTransport::class ],
            \WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler::class,
            null,
            [
                'craftforms/create-form',
                'craftforms/add-field',
            ]
        );
    } );
    

    The last argument is the allowlist. Only abilities named there become tools on this server — everything else on the site stays invisible to it. If you want an agent that can create forms but never add fields, remove a line.

    The namespace and route arguments determine the endpoint: craftforms-mcp + mcp gives you https://yoursite.com/wp-json/craftforms-mcp/mcp.

    Confirm it registered:

    wp mcp-adapter list
    
    ID           Name                    Version  Tools  Resources  Prompts
    craftforms   CraftForms MCP Server   1.0.0    2      0          0
    

    Two tools. That’s the pair.

    Step 3 — Look at what the agent will see

    Before wiring up a client, it’s worth seeing the tool definitions the way a model receives them. The adapter includes a STDIO transport you can drive from the command line:

    echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
      | wp mcp-adapter serve --user=admin --server=craftforms
    

    The response includes both tools with their full input schemas. Trimmed to the essentials:

    {
      "name": "craftforms-add-field",
      "title": "Add a Field to a Form",
      "description": "Appends a new input field block to an existing CraftForms form.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "form_id":    { "type": "integer" },
          "field_type": { "type": "string",
                          "enum": ["text","email","number","textarea",
                                   "select","radio","checkboxes"] },
          "name":       { "type": "string" },
          "label":      { "type": "string" },
          "required":   { "type": "boolean" },
          "options":    { "type": "array" }
        },
        "required": ["form_id","field_type","name","label"]
      }
    }
    

    Two details worth catching here, because both will bite you otherwise:

    Tool names are slugified. The ability craftforms/create-form becomes the MCP tool craftforms-create-form. Slashes aren’t legal in MCP tool names, so the adapter converts them. When you call a tool, use the hyphenated form.

    The enum is the contract. field_type accepts exactly seven values. A model that hallucinates "date" gets a validation error back rather than a broken form — which is the entire point of schema-typed abilities over prompt-and-hope generation.

    Step 4 — Connect a client over HTTP

    STDIO is good for testing. For a real client you want the HTTP transport, which is the endpoint the server registration created.

    Authentication is standard WordPress — an application password is the practical choice. Create one under Users → Profile → Application Passwords, or:

    wp user application-password create admin mcp-client --porcelain
    

    MCP over HTTP is a session protocol, so the first call is a handshake:

    curl -i -X POST "https://yoursite.com/wp-json/craftforms-mcp/mcp" \
      -u "admin:xxxx xxxx xxxx xxxx xxxx xxxx" \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
           "params":{"protocolVersion":"2025-06-18","capabilities":{},
                     "clientInfo":{"name":"curl","version":"1.0"}}}'
    

    The response headers carry the session:

    HTTP/2 200
    mcp-session-id: 3d75124b-3f55-49be-a28b-cadb7b0eb633
    
    {"jsonrpc":"2.0","id":1,"result":{
      "protocolVersion":"2025-06-18",
      "serverInfo":{"name":"CraftForms MCP Server","version":"1.0.0"},
      "instructions":"Build and configure CraftForms forms from an AI agent."}}
    

    Every subsequent request must carry Mcp-Session-Id. Miss it and you get -32600 Invalid Request: Missing Mcp-Session-Id header, which is the single most common first-run stumble.

    A real MCP client handles all of this for you. In Claude Code, for example, you’d add the server once and never think about sessions again:

    claude mcp add --transport http craftforms \
      https://yoursite.com/wp-json/craftforms-mcp/mcp \
      --header "Authorization: Basic $(printf 'admin:xxxx xxxx xxxx' | base64)"
    

    Step 5 — Let the agent build the form

    Now the payoff. With the server connected, you ask in plain language:

    “Build me a quote request form with the customer’s name, email, a service dropdown for Basic / Pro / Enterprise, and a project details box. Name and email required.”

    The agent calls craftforms-create-form first:

    {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
      "name":"craftforms-create-form",
      "arguments":{"title":"Quote Request","description":"Service quote request"}}}
    
    {"jsonrpc":"2.0","id":1,"result":{
      "structuredContent":{
        "form_id":932,
        "uuid":"form_6a953ab5dc2c6",
        "edit_url":"https://yoursite.com/wp-admin/post.php?post=932&action=edit"},
      "isError":false}}
    

    Then one craftforms-add-field call per field, threading form_id: 932 through each. Four calls later the form exists, is published, and opens in the block editor at that edit_url.

    Note the output schema is doing real work here. The agent gets back a structured form_id and edit_url, not prose it has to parse. That’s what lets it chain calls reliably instead of guessing.

    Why this beats “paste a prompt into a textbox”

    Here’s what the agent actually wrote into the post, for the email field:

    <!-- wp:craftforms/text-input-field {"name":"email","label":"Email Address","required":true,"type":"email"} -->
    <!-- wp:craftforms/label {"content":"Email Address"} /-->
    
    <!-- wp:craftforms/text-input {"name":"email","required":true,"type":"email","fontSize":"medium","style":{"spacing":{"padding":{"top":"var:preset|spacing|20","bottom":"var:preset|spacing|20","left":"var:preset|spacing|20","right":"var:preset|spacing|20"}}}} /-->
    
    <!-- wp:craftforms/form-error {"content":"{{error.email}}","style":{"typography":{"fontSize":"0.8em"}}} /-->
    <!-- /wp:craftforms/text-input-field -->
    

    That’s correct Gutenberg block markup, with matched attributes, the right nesting, a bound error block, and theme spacing presets. The model didn’t generate it — the ability did. The model supplied four values (name, label, required, field_type); PHP built the markup.

    This is the structural difference, and it’s the whole argument:

    • A prompt-to-HTML AI builder asks a language model to produce block markup. Sometimes it’s valid. Sometimes you get “This block contains unexpected or invalid content” in the editor, because a single mismatched attribute between the comment JSON and the saved HTML fails Gutenberg’s block validation.
    • An ability takes typed arguments and generates the markup deterministically, in code that was tested. The model can only get the intent wrong, never the syntax.

    The same holds for everything the ability handles on your behalf: required-field validation, aria-describedby wiring, role="group" on option sets, unique field names. You get accessibility and validation correct by default because a human wrote that part once, not because a model remembered to.

    And because permission callbacks are part of the ability definition, the agent inherits WordPress’s own access rules. craftforms/create-form checks the CraftForms access capability; craftforms/add-field checks edit_post against the specific form. An agent authenticated as a Subscriber can enumerate the tools and accomplish nothing with them.

    The other half: filling a form with an agent

    “Build a form” is where every competitor article stops. But the more interesting automation is the reverse — an agent that submits to your form. Order intake from an email parser, a nightly job that pushes leads from another system, an internal assistant that files a request on someone’s behalf.

    CraftForms exposes submission as a plain REST endpoint:

    POST /wp-json/craftforms/v1/submit/{form_uuid}
    

    The {form_uuid} is the value create-form handed back (form_6a953ab5dc2c6 above). Field values go at the top level of the JSON body — not nested under a data or formData key, which is the mistake everyone makes first:

    curl -X POST "https://yoursite.com/wp-json/craftforms/v1/submit/form_6a953ab5dc2c6" \
      -H "Content-Type: application/json" \
      -d '{
            "full_name": "Ada Lovelace",
            "email":     "[email protected]",
            "service":   "pro",
            "details":   "Need a quote for Q4"
          }'
    
    {"success":true,"data":{"successMsg":"We have received your message. Thank you!"}}
    

    A real submission: it runs server-side validation, recalculates any price formula rather than trusting the payload, saves the entry, and fires the form’s submit actions — emails, webhooks, PDF generation, whatever’s configured. It’s the same code path a browser submit takes.

    The security model you need to understand first

    That request only succeeded because the form was explicitly configured to allow it. The endpoint has a gate, and it’s worth walking through because it’s the difference between “agent-fillable” and “open spam relay.”

    CraftForms classifies every submission by Origin/Referer:

    Same-origin submissions (a browser on your own site) require a valid nonce. An agent driving a real browser gets this for free — the nonce is in the rendered page. An agent making a bare HTTP call from your own domain without one gets Invalid security token.

    Off-site submissions — which is what an agent, a script, or a static site is making — are rejected by default. To allow them you must:

    1. Be on CraftForms Pro. Without a licence: External submissions require a PRO licence.
    2. Enable external submissions on that specific form. It’s per-form, not global. Without it: External submissions are not enabled for this form.
    3. Optionally, add required header rules — key/value pairs the caller must send. This is a shared secret, so an agent can submit and a random script that discovered the UUID cannot.

    That third step is the one to actually use. A form UUID isn’t a secret — it’s in the page markup. If you’re going to let machines post to a form, put a header behind it:

    curl -X POST "https://yoursite.com/wp-json/craftforms/v1/submit/form_6a953ab5dc2c6" \
      -H "Content-Type: application/json" \
      -H "X-Agent-Token: your-shared-secret" \
      -d '{"full_name":"Ada Lovelace","email":"[email protected]","service":"pro"}'
    

    Be honest with yourself about what this is: shared-secret authentication plus origin checks. It’s appropriate for an agent you control calling a form you control. It is not a substitute for a captcha on a public contact form, and CraftForms doesn’t pretend otherwise.

    Where the ceiling is

    Two abilities is a deliberate starting point, not a finished surface. Worth knowing what falls outside it before you plan around it:

    • Seven field types. text, email, number, textarea, select, radio, checkboxes. The agent can’t add a date picker, file upload, payment block, or booking calendar. Those exist in CraftForms — you add them in the editor.
    • No editing or deleting. add-field appends. There’s no ability to reorder, modify, or remove a field. An agent can build up; it can’t refactor.
    • Pricing is create-time only. create-form accepts a formula and transformations, so an agent can scaffold a calculating form — but only in the initial call. There’s no ability to adjust pricing afterwards.
    • The adapter is young. MCP Adapter 0.6.1 at the time of writing, distributed from GitHub rather than the plugin repository. Treat it as a capable pre-1.0 tool, not a set-and-forget production dependency.

    The honest framing: this gets an agent from nothing to a real, valid, publishable form in a handful of tool calls. Then you open the editor and finish it. For scaffolding — a dozen similar intake forms, a client’s ten department contact forms — that’s a genuinely large chunk of the work gone. For a complex booking or product configurator, it’s the first five minutes.

    If you hit that ceiling, there’s a second path. CraftForms publishes its full authoring specification as the AI Form Builder Guide. Hand that to a coding agent with shell access to a local install and it can build anything the plugin does — date pickers, repeaters, conditional logic, WooCommerce — then export the result as a single file you import on production. Slower to set up, no ceiling, and the agent never touches your live site.

    Running WordPress older than 6.9?

    The abilities simply don’t register — the core hooks they attach to don’t exist, and CraftForms checks for wp_register_ability() before doing anything. No error, no MCP.

    The fallback is WP-CLI. Forms are a custom post type with block content and a meta array, so an agent with shell access can scaffold one with wp post create and wp post meta update. It’s a lot more finicky — you’re hand-writing the block markup the ability would have generated, and getting a block-comment attribute out of step with the saved HTML is exactly the validation failure the ability exists to prevent. Upgrade to 6.9+ if you can.

    Try it

    Everything above runs on a stock install:

    1. WordPress 6.9+ with CraftForms (free) — the abilities register themselves.
    2. wp plugin install https://github.com/WordPress/mcp-adapter/releases/latest/download/mcp-adapter.zip --activate
    3. Drop in the mu-plugin from Step 2.
    4. wp mcp-adapter list to confirm two tools.
    5. Point your client at https://yoursite.com/wp-json/craftforms-mcp/mcp with an application password.
    6. Ask for a form.

    The interesting part isn’t that an AI made a form. It’s that nothing in that chain is proprietary to CraftForms — core registry, core-project adapter, open protocol. Any plugin can register abilities. Any client can call them. That’s a considerably better bet than an API key field in a settings screen.


    Related: Answer-Engine Optimization for Form & Quote Pages · How to Use WordPress as a Form Backend for Static Sites and Web Apps · Use WordPress as a Locked-Down Form Backend


  • How to Build a Seasonal Boat Rental Booking Form

    How to Build a Seasonal Boat Rental Booking Form


    TL;DR

    • Boat rental doesn’t fit a plain date-and-price form: the price shifts by season (July costs more than April) and the boat is only available in half-day time slots, not “all day.”
    • CraftForms’ booking datepicker has a seasonal mode built for exactly this — you define date ranges (“seasons”), and each season gets its own weekly schedule of time slots. Peak season can offer more, shorter slots; shoulder season fewer, longer ones.
    • An expression Smart Variable (mnth = month(booking.date)) reads the month straight out of whatever date the customer picks, and feeds a Table Smart Variable that returns that month’s price — no separate forms, no manual date-range pricing rules.
    • The Boat Rental (Seasonal) starter ships this pattern ready-built: per-boat pricing, capacity-capped quantity. It’s a Pro starter.
    • A Catalog resource attached to the form tracks per-date/per-slot availability automatically, so a confirmed booking blocks that slot — no manual availability spreadsheet. Getting the cap to actually enforce per-boat (not just per-booking) takes one extra setting, covered below.
    • This form’s companion — a shared, skippered trip priced per person instead of per boat — gets its own walkthrough in a separate post, since the pricing and capacity mechanics there are different enough to deserve their own space.

    Boat rental is a good stress test for a booking form, because it breaks two assumptions most generic date-picker plugins make. First, the price isn’t fixed — a half-day out on the water in July isn’t priced the same as one in April, because demand (and often fuel, staffing, and insurance costs) swings by season. Second, availability isn’t “free or booked for the day” — a boat goes out in half-day slots, and how many slots exist on a given day depends on the season too: peak summer might run three tighter slots, shoulder months just two longer ones.

    A contact form can’t calculate a month-aware price. A generic date picker can’t offer three time slots in August and two in April on the same calendar. CraftForms handles both with two features working together: seasonal booking mode and a month-keyed price table. Rather than explain that abstractly, this post walks through the actual starter form that ships with the plugin — Boat Rental (Seasonal) — field by field, formula by formula.

    Cf boat rent august
    Cf boat rent september

    Same field, same calendar widget — the number of bookable times per day changes depending on which season the selected date falls into. That’s the whole point of seasonal mode.


    Why This Doesn’t Fit a Plain Booking Form

    Before opening the starter, it’s worth being specific about what a “simple” booking form gets wrong here:

    • One price for the whole season. If you hardcode a single day-rate, you either underprice July or overprice April. Neither is sustainable for a seasonal business.
    • One time-slot schedule for the whole year. If your quietest month and your busiest month share the same slot times, you’re either wasting capacity in August or offering slots nobody wants in April.
    • No capacity ceiling. A rental fleet has a finite number of boats (or one boat, in this starter’s case) available per slot. Without a hard cap tied to that inventory, the form will happily accept a booking you can’t fulfil.

    The starter solves all three with two building blocks: the datepicker’s seasonal mode, and a Table Smart Variable keyed by month. Let’s open it.


    Walkthrough — Boat Rental (Seasonal)

    This is the self-drive starter: a customer rents one or more boats for a half-day slot and drives themselves. Here’s what’s actually in the form.

    The seasonal datepicker

    The Rental date & time slot field (booking) is a booking-datepicker-field set to Field Type: "seasonal". Instead of one weekly schedule for the whole year, it defines five seasons, each with its own date range and its own weekly time-slot schedule:

    SeasonDate rangeTime slots (every day)
    April–May04-01 to 05-3110:00, 14:00
    June06-01 to 06-3009:30, 14:00
    July–August07-01 to 08-3108:30, 12:00, 15:30
    September09-01 to 09-3009:30, 14:00
    October10-01 to 10-3110:00, 14:00

    Notice the shape: shoulder months (April–May, September, October) get two half-day slots a day, June gets its slots nudged half an hour earlier, and peak season (July–August) gets a third slot squeezed in at 08:30/12:00/15:30. That’s the whole point of seasonal mode — the schedule itself changes by date range, not just the price. Each season is defined once, with a start/end date and a per-day-of-week list of enabled times, and the picker only shows the times valid for whichever season the selected date falls in.

    If you need to restrict how last-minute someone can book, or how far out they’re allowed to book at all, the datepicker’s advance-notice and bookable-date-window settings apply on top of seasonal mode the same way they do on every other booking mode — they’re not something you rebuild per season.

    The field alone doesn’t check availability or capacity. The seasons you just defined on the datepicker field make it look right — the correct months, the correct times per season — but on their own they’re purely a schedule display. Without a connected Catalog resource, the picker never asks the backend whether a date/time is already booked, so every slot always shows as available, and the quantity cap in the next section has nothing real to bind to. This is a simplified/demo-friendly booking mode, not a capacity-enforcing one — CraftForms shows this same warning directly in the field’s Seasonal Time Slots panel. Real availability and capacity checking is a Catalog-resource feature, covered next.

    Setting it up on a Catalog resource

    The seasons table above lives on the form’s datepicker field by default, and the field works on its own for a purely cosmetic seasonal calendar. But for the quantity cap (next section) to mean anything, you need real inventory behind it — a Catalog resource, a separate “boat” or “fleet” entry that tracks how many units are actually available per slot. Under CraftForms → Catalog, create a booking-type item and set:

    Cf booking
    • Booking Type: Seasonal Time Slots (Rentals / Tours)
    • Availability Window: how far out customers can book (the starter’s default of 0–30 days is a sane starting point; widen it for a business that takes bookings further ahead)

    Then, under Seasons, define the same five date ranges and per-weekday time slots as the table above. Once this resource is connected to the form, its seasons are the ones that actually drive the calendar — the datepicker field’s own seasons data is ignored entirely in favor of the resource’s, so there’s no need to keep two copies in sync. Define seasons once, here, on the Catalog item:

    Cf booking

    Now, after we set availability days and times inside the catalog item – we can remove this data from datepicker form field itself. There is no need to keep duplicates. Besides, the data from the form field will not be used for the form connected to this catalog item. Attention: the datepicker form field must be referenced inside the catalog item too (for this data to be used). We will do it soon. Keep reading.

    The quantity field, capped by capacity

    Right under the datepicker sits a number field named qty — “How many boats do you want to rent?” It carries a Max Value (expression) of _booking_capacity, set in the field’s Dynamic Validation panel (the panel that lets a field’s validation react to live data instead of a fixed number).

    Cf dynamic validation

    That expression binds the field’s maximum to whatever capacity the connected Catalog resource reports for the selected slot. The customer physically cannot request more boats in a slot than the resource says are available — CraftForms enforces it as a live max on the field, not a manual note in the label.

    Getting that cap to actually count boats correctly takes one more setting on the Catalog resource, and it’s the part that’s easy to miss: by default a resource’s Occupancy Model is “Exclusive,” meaning the whole slot is marked taken by a single booking regardless of how many boats that booking asked for — so five separate customers could each book qty=5 in the same slot before anything blocks them. You want Shared occupancy instead, plus a Capacity Measure Variable that names the field holding the quantity to deduct:

    Cf booking
    Cf booking

    With Occupancy Model: Shared and Capacity Measure Variable: qty, a booking of qty=3 correctly deducts 3 boats from that slot’s capacity of 5 — leaving 2 for the next customer — instead of just marking the slot “used” after the first booking. This is also where the Catalog resource earns its keep beyond pricing: once a booking is confirmed, CraftForms marks that date/slot’s remaining capacity accordingly, so the next visitor sees accurate availability automatically. No separate spreadsheet, no manually blocking out dates after every phone booking.

    Pricing by month: the mnth variable and the price table

    Here’s the part that makes the whole thing tick. Open Smart Variables on this form and there are two entries doing the pricing work:

    Cf smart variables list

    mnth — an expression variable:

    Cf smart variable
    month(booking.date)
    

    booking is the name of the datepicker field, and .date pulls the date portion out of whatever the customer picked. The month() function returns just the numeric month (4 for April, 7 for July, and so on). This one line is what lets the rest of the form price “by season” without ever touching a season’s name — it just needs the month number.

    boat_price — a table variable keyed on mnth, using the exact match lookup strategy:

    Cf smart variable
    MonthPrice
    4€130
    5€130
    6€145
    7€160
    8€160
    9€145
    10€120

    The table’s row and column variable are both set to mnth, and the lookup strategy is exact match — the resolved month has to match a row precisely, which is correct here since every month in the operating season has its own explicit price. (Contrast this with a “closest” strategy, which you’d use for tiered pricing where in-between values should round to the nearest defined tier — not needed here since every month is already listed.)

    The form’s Price Formula ties it together in one line:

    Cf price formula
    boat_price * qty
    

    Pick a date in August, mnth resolves to 8, boat_price looks up €160, multiply by however many boats the customer requested, and that’s the total — recalculated the instant the date or quantity changes, and re-verified on the server at submission so nothing the customer could tamper with in the browser makes it to checkout.

    The total shows live in an Info block near the bottom of the form:

    Total: €{{form.price}}
    

    next to a plain-language note — “Half-day rental (4 hours) · price per boat varies by season” — so nobody is surprised the number changes when they pick a different month.


    Taking Payment and Sending a Confirmation

    This starter uses the same payment and confirmation pattern covered in our other booking and order-form guides:

    1. Connect Stripe. Go to CraftForms → Payment Settings, enter your Stripe publishable and secret keys (test keys first), set your currency, and add the webhook URL CraftForms gives you to your Stripe dashboard. The webhook is what moves an order from pending to paid once Stripe confirms the charge.
    2. Add the payment block. Drop the CraftForms Payment block into the form, typically just above the submit button. Without it, the form still calculates and displays a price, but it won’t actually charge anyone.
    3. Full payment or a deposit — it’s the same mechanism, just a different formula. CraftForms doesn’t have a separate “deposit” feature; if you want to collect a 30% deposit instead of the full charter price up front, write it into the Price Formula itself, e.g. round(boat_price * qty * 0.3, 2). The rest of the payment flow — Stripe keys, webhook, order status — doesn’t change.
    4. Confirmation email. In the form’s action settings, add an email action addressed to {{email}}, referencing any field with {{field_name}} tags ({{qty}}, {{booking}}, {{form.price}}, etc.). Pick a saved Email Template if you want a branded layout instead of plain text.
    5. Track it in Orders. Every submission shows up in CraftForms → Orders with the customer’s details, the calculated price, the Stripe reference, and its status — pending → confirmed → paid → refunded — so you’re not reconciling bookings against a separate spreadsheet.

    Making It Yours

    This starter is deliberately built around one boat so the pricing logic stays readable, but nothing about the mechanism is fixed to boats specifically:

    • Change the seasons. Add, remove, or resize the date ranges to match your actual operating calendar — a five-season year isn’t a requirement, it’s just what this boat happens to run. Once the datepicker field is connected to a Catalog resource, the resource’s seasons are the ones that actually count — the field’s own Seasonal Time Slots panel still shows a seasons editor, but its values are ignored at render time in favor of the connected resource’s. Edit seasons on the Catalog item, not the field.
    • Change the slot times and density per season. Nothing stops you from running four short slots in peak season and one long slot the rest of the year, or closing certain days of the week entirely (each day in a season’s schedule can be disabled independently).
    • Change the price table’s numbers, or add more rows if your season runs longer than April–October.
    • Change the capacity logic. The Shared occupancy model plus a Capacity Measure Variable is just a Smart Variable/Catalog pattern — point the Capacity Measure Variable at whatever field holds your quantity, and set the resource’s capacity to match your real fleet size.

    To load this starter yourself: when creating a new CraftForms form, the Choose form starter modal opens automatically the first time (it also lives permanently under Starter Templates in the block editor’s document sidebar, in case you dismiss it or come back later). Pick Boat Rental (Seasonal) from the list, and everything above — seasons, price table, and capacity fields — is already sitting there for you to inspect and adjust.


    Summary

    Here’s what this starter demonstrates:

    1. A seasonal booking datepicker mode where each date range gets its own weekly schedule of time slots — denser in peak season, sparser in shoulder months
    2. An expression Smart Variable (month(booking.date)) that reads the month straight out of the picked date
    3. A Table Smart Variable, keyed on that month, that resolves the correct season’s price automatically
    4. Capacity enforcement via a Dynamic Validation Max Value expression bound to _booking_capacity, backed by a Catalog resource set to Shared occupancy with a Capacity Measure Variable pointing at the quantity field — the setting that makes the cap count boats instead of just bookings
    5. The same Stripe payment and confirmation-email pattern used across every other CraftForms booking form, including writing a deposit as a fraction of the formula rather than a separate feature

    Install CraftForms Pro, load the Boat Rental (Seasonal) starter, and you’re looking at a complete seasonal rental booking form — pricing, availability, and capacity all included — ready to point at your own boats, seasons, and prices.

    Running a shared, skippered trip instead of a self-drive rental? The same seasonal mechanism, priced per person with a shared headcount cap, is covered in the companion post on the Boat with Skipper (Seasonal) starter.

  • Add Forms to a Hugo Site With a WordPress Backend

    Add Forms to a Hugo Site With a WordPress Backend

    Hugo builds sites at a speed that makes everything else feel slow. It also, by design, has no idea what to do with a form submission — it’s a static generator, and there’s no server on the other end. The moment your Hugo site needs a contact form, a booking, or a quote request, you’re shopping for a backend.

    You can point that form at a hosted endpoint and hope you don’t outgrow its limits. Or you can use a backend you fully control: a locked-down WordPress install running CraftForms, serving forms to your Hugo site and handling every submission. Same fast Hugo frontend, a real form engine behind it.

    For the full architecture and how to lock the WordPress side down, see Use WordPress as a Locked-Down Form Backend for Static Sites. This post is the Hugo integration.


    How it fits together

    Build the form once in WordPress. On your Hugo pages, use a shortcode (in content) or a partial (in templates) that outputs a small placeholder. At runtime embed.js fetches the live form from your backend and renders it. Submissions post back to WordPress, which validates, stores, and routes them.

    The form isn’t baked into your Hugo build, so editing it in WordPress needs no rebuild.


    Setup

    1. Build the form in CraftForms

    A normal CraftForms form — fields, conditional logic, validation, email notifications, and payments if you need them.

    2. Enable external submissions + create an embed key

    Submission settings → Allow External Submissions, then CraftForms → Settings → Embed → create a key bound to your Hugo site’s domain. Copy it.

    3. Install the Hugo helpers

    Copy these from the plugin’s examples/static-site/hugo/ into your Hugo project, preserving structure:

    layouts/shortcodes/craftform.html   → use from Markdown content
    layouts/partials/craftform.html     → use from templates
    

    Set your backend URL once in hugo.toml:

    [params.craftforms]
      wpUrl = "https://forms.example.com"
    

    4. Use it

    From a content file:

    ## Get in touch
    
    {{< craftform key="aHR0cHM6Ly9mb3Jtcy5leGFtcGxlLmNvbQ.abc123" >}}
    

    With a catalog item for pricing:

    {{< craftform key="…" resourceId="42" >}}
    

    From a template (e.g. a booking layout):

    {{ partial "craftform.html" (dict "key" .Params.formKey "resourceId" .Params.catalogItem) }}
    

    Both emit the same snippet:

    <div data-craftforms-embed="…"></div>
    <script src="https://forms.example.com/wp-content/plugins/craftforms/build/webcomponents/embed.js" defer></script>
    

    What this buys you over a hosted endpoint

    • No submission caps — it’s your database.
    • Conditional logic, file uploads, real validation — enforced server-side, not just hinted in the browser.
    • Branded confirmation and notification emails, with attachments.
    • Payments via Stripe inside the form.
    • Data ownership — submissions never leave infrastructure you control.
    • Multiple forms, one backend — every Hugo site you run can share it.

    Security notes

    • The embed key is domain-bound; the backend validates request Origin.
    • Prices, stock, and capacity are re-verified server-side on submit.
    • The WordPress install is locked down to a near-zero attack surface.
    • No built-in CAPTCHA yet — add a required request header to the form as a shared-secret spam gate on public endpoints.

    Same idea, other stacks

    If you also build with Astro or Builderius, the mechanism is identical — an Astro <CraftForm /> component and a Builderius shortcode both emit the same embed placeholder. One backend, one embed model, every static toolchain.

    CraftForms embedding and external submissions are CraftForms PRO features.

  • Add a Real Form Backend to Your Astro Site (Not Just Email)

    Add a Real Form Backend to Your Astro Site (Not Just Email)

    Astro is a joy to build with, right up until you need a form. Then you hit the wall every static-site developer hits: there’s no server to receive the submission. The quick fix is a hosted endpoint that emails you the result — fine for a single contact form, painful the moment you need many forms with real logic.

    Take a summer camp site. It needs a registration form, a medical form, a photo-release form, a liability waiver, maybe a payment. Some fields are required only if an earlier answer was “yes.” Files get uploaded. Confirmation emails go out. A mailto-style endpoint can’t do any of that, and wiring five of them up — each with its own dashboard and submission cap — is its own small nightmare.

    There’s a better shape: keep Astro static and fast, and point every form at one WordPress backend running CraftForms. WordPress does what it’s good at (storing, validating, routing, emailing); Astro does what it’s good at (serving fast static pages). Here’s how to connect them.

    This is the Astro-specific walkthrough. New to the whole idea? Headless WordPress + a Static Frontend, Explained covers why this shape works before you wire it up. And to lock the WordPress backend down to near-zero attack surface, see Use WordPress as a Locked-Down Form Backend for Static Sites.


    How it works

    You build the form once in WordPress with CraftForms. On your Astro page you drop a small component that renders a placeholder. At runtime a tiny script (embed.js) fetches the live form from your backend and boots it in place. Submissions post back to the backend, which validates and stores them.

    Nothing about your form lives in the Astro build — so you can edit fields, validation, or pricing in WordPress and the change appears without rebuilding the site.


    Why it stays fast

    The Astro page was already instant — it’s static. The form doesn’t change that. Once it boots, everything the visitor does happens in the browser: conditional fields show and hide, and if it’s a product form, the price recalculates on every option change with no backend call. The only time the backend is touched is the final submit. So a camper’s parent can work through a multi-field registration, toggling options and watching the total update, without a single round-trip — and your WordPress backend does almost nothing per visit. Fast page, fast form, quiet server.


    Setup

    1. Build the forms in CraftForms

    Registration, medical, release, waiver — each is a normal CraftForms form. Add conditional logic (show the guardian-signature field only for under-18s), file upload fields, and email notifications. This is standard CraftForms; nothing Astro-specific yet.

    2. Enable external submissions + create an embed key

    For each form: Submission settings → Allow External Submissions. Then CraftForms → Settings → Embed → create an embed key bound to your Astro domain (e.g. camp.example.com). Copy each key. The key encodes your WordPress URL, so the Astro side never hard-codes it.

    3. Add the component

    Copy CraftForm.astro from the plugin’s examples/static-site/astro/ into src/components/. Set your backend URL once in .env:

    PUBLIC_CRAFTFORMS_WP_URL=https://forms.example.com
    

    Then use it anywhere:

    ---
    import CraftForm from "../components/CraftForm.astro";
    ---
    
    <h1>Camp registration</h1>
    <CraftForm embedKey="aHR0cHM6Ly9mb3Jtcy5leGFtcGxlLmNvbQ.abc123" />
    
    <h2>Photo release</h2>
    <CraftForm embedKey="aHR0cHM6Ly9mb3Jtcy5leGFtcGxlLmNvbQ.def456" />
    

    Multiple forms on one page are fine — the loader upgrades every placeholder and injects each shared asset only once.

    Under the hood the component emits:

    <div data-craftforms-embed="…"></div>
    <script is:inline defer src="https://forms.example.com/wp-content/plugins/craftforms/build/webcomponents/embed.js"></script>
    

    is:inline tells Astro to leave the external loader alone rather than bundling it.


    What you get that a hosted endpoint can’t do

    • Conditional logic — fields that appear based on earlier answers, all evaluated in the browser.
    • File uploads — waivers, medical documents, ID photos, stored on your backend.
    • Validation that means something — required fields, formats, min/max, custom rules, enforced server-side.
    • Real email — branded confirmations to the parent, notifications to staff, with attachments.
    • Payments — take a deposit with Stripe inside the same form.
    • Your data, your server — submissions live in your WordPress database, not a third party’s, with no monthly submission cap.

    Is it secure?

    The submission endpoint is public — that’s the point — but it isn’t naive:

    • The embed key is bound to your domain; the backend checks the request Origin and refuses others.
    • Anything money- or capacity-related (price, stock, booking slots) is re-computed server-side and rejected if tampered.
    • The WordPress install itself is locked down: one plugin, no public pages, no XML-RPC, hidden login.

    On spam: a public endpoint attracts bots, but CraftForms runs a built-in anti-spam check on every submission, so you’re covered out of the box — no CAPTCHA to bolt on. If you want an extra layer for a high-traffic public form, require a request header (Submission settings → Required headers) that only your embed sends — a lightweight gate most drive-by bots won’t clear.


    The result

    Your camp site stays a static Astro build — instant to load, trivial to host. But every form on it is a real form: logic, files, email, payments, and submissions you own. One backend serves them all, and editing a form never means redeploying the site.

    Embedding and external submissions are CraftForms PRO features.

  • Build Branded Email Templates in WordPress

    Build Branded Email Templates in WordPress


    TL;DR

    • CraftForms has a visual email template builder inside the WordPress block editor — no HTML, no coding.
    • Add dynamic tags like {{email.name}}, {{email.checkin_date}}, or {{email.price}} and they fill in with real submission data on send.
    • Connect the template to any form via the Send Email Template action.
    • Add Mailtrap’s free sandbox as an SMTP server to catch test emails in a browser inbox — nothing reaches a real address while you’re still building.
    • Check CraftForms → Email Logs to confirm every send: which server delivered it, whether it succeeded, and the full email body.

    A booking confirmation lands in the guest’s inbox five seconds after they submit your form. It has your logo at the top, the check-in and check-out dates they chose, the total they’re paying, a button to add the stay to their calendar. It looks like it came from a real hospitality business — because it did.

    That’s not a third-party email marketing tool. It’s CraftForms’ built-in email template builder, and it took about twenty minutes to set up.

    Visual email template builder is a FREE version functionality! ❤️

    Most WordPress contact form plugins send a notification that looks like this: a plain white email, monospace font, a dump of every field value, your WordPress site URL at the bottom. It gets the information across. It also tells your customer they’re dealing with a website held together with default settings.

    Your confirmation email is often the first thing a customer receives from you after making a booking or placing an order. It’s the moment when the transaction becomes real for them. A branded, well-structured email builds trust, reduces the “did it actually go through?” anxiety, and sets the tone for everything that follows.

    CraftForms includes a visual template builder for this — inside the WordPress block editor you already know. Here’s how to use it.


    What the CraftForms email template builder actually is

    The template builder isn’t a custom editor bolted onto the plugin. It’s the native WordPress block editor — the same Gutenberg interface you use to write posts and pages — applied to email.

    You add blocks: headings, paragraphs, images, buttons, columns, spacers. You set colours and typography. You arrange sections visually. When you save, CraftForms automatically compiles the result into email-safe HTML: CSS is inlined so it renders correctly in Gmail, Outlook, and Apple Mail. You never touch a line of code.

    The right sidebar gives you tools that are specific to email:

    • Starter Templates — a library of pre-built designs to start from instead of a blank canvas
    • Email Template Preview — opens a modal showing exactly how the compiled email renders
    • Dynamic Data — a panel that lists all available dynamic tags for the template, including every field from any connected form
    • Email Settings — set a default subject line and recipient address directly in the template
    • Styles — background colour for the email body

    The result is a WYSIWYG email editor with the full capability of the block editor behind it.


    Step 1: Create a new email template

    Go to CraftForms → Email Templates in your WordPress admin and click Add New.

    The block editor opens. Before you start designing from scratch, click Starter Templates in the right sidebar. A modal shows a selection of pre-built designs — booking confirmations, order receipts, contact acknowledgements. Pick one that’s close to what you need and click Insert — it loads into the editor as fully editable blocks.

    CraftForms email template editor with a starter template loaded — header section, body text, CTA button, and the Starter Templates panel visible in the sidebar
    Craftforms email template builder
    CraftForms email templates builder

    Give the template a name using the document title field at the top (e.g. “Booking Confirmation — Guest”).


    Step 2: Design the template visually

    Edit the template exactly as you would a WordPress page. Click any block to select it, then use the block toolbar and right sidebar to adjust it.

    Useful patterns for confirmation emails:

    • Full-width header block with your site name or logo image and a headline like “Your booking is confirmed”
    • Two-column layout for booking details — label on the left, value on the right (e.g. “Check-in” / {{email.checkin_date}})
    • A prominent button block linking to your site, your cancellation policy page, or a calendar download
    • Footer section with your contact details and a note about how to get in touch

    For colours, use the Styles panel in the right sidebar to set the email background. Individual blocks follow the standard block editor colour controls.

    When you want to see how it all looks as a real email, click Open the preview in the “Email Template Preview” sidebar panel. A modal opens with the fully compiled, CSS-inlined HTML rendered as it will appear in an email client.


    Step 3: Add dynamic tags

    This is where the template goes from a generic design to a personalised confirmation. Dynamic tags are placeholders that CraftForms replaces with actual submission data when the email is sent.

    Open the Dynamic Data panel in the right sidebar. It lists all the tags available for the template, organised by category.

    Site tags (always available, no form required):

    • {{site.name}} — your WordPress site name
    • {{site.url}} — your site URL
    • {{site.admin_email}} — the admin email address

    Form field tags (appear once you connect a form — see Step 4):

    • {{email.field_name}} — the value of any named field in the form
    • For booking date pickers: {{email.checkin_date}}, {{email.checkout_date}}
    • For single-date pickers: {{email.date}}, {{email.time}}
    • For price: {{email.price}}, {{email.currency}}
    • Smart Variables (calculated fields marked as exposed): {{email.variable_name}}

    Click any tag in the panel to copy it, then paste it into a text block in the editor. The tag appears as literal text while you’re editing — it’s replaced with the real value at send time.

    Craftforms dynamic data email template
    Dynamic Data panel in Email Template builder

    A practical booking confirmation body might look like this:

    Hi {{email.first_name}},

    Your booking at {{site.name}} is confirmed.

    Check-in: {{email.checkin_date}} Check-out: {{email.checkout_date}} Total: {{email.price}} {{email.currency}}

    We look forward to welcoming you. If you need to make any changes, reply to this email.

    The subject line and default recipient address go in the Email Settings panel. The subject supports tags too — Booking confirmed — {{email.first_name}} works as a subject line.

    When you’re happy, click Save. CraftForms compiles the template to email-safe HTML in the background. It’s ready to use.


    Step 4: Connect the template to a form

    Open the form you want to trigger this email, or create a new one. In the form editor, go to the Actions section (the tab or panel that lists what happens after submission).

    Add a Send Email Template action if one isn’t already there. Configure it:

    1. Email Template — select the template you just created from the dropdown
    2. Recipient — enter the email address to send to, or use a tag like {{email.email_field}} to send to the address the user entered in the form
    3. SMTP Server — tick Use custom SMTP server and select your SMTP server from the dropdown (we’ll add the Mailtrap sandbox server next)
    Craftforms send email template submit action
    Send Email Template submit action with email template chosen

    Save the form.


    Step 5: Test with Mailtrap’s free sandbox

    Before this email reaches any real address, test it. Mailtrap is a free service that acts as a catch-all SMTP server for testing: any email your site sends to it gets intercepted and displayed in a web interface — you see the full HTML render, the raw source, the headers, and a spam score. Nothing lands in a real inbox.

    Set up a Mailtrap account:

    1. Sign up at mailtrap.io — the free plan gives you up to five sandbox inboxes with 1,000 test emails per month, no credit card required.
    2. In the Mailtrap dashboard, go to Sandboxes → Add Sandbox to create a new one. Then click on that newly created inbox to access its settings.
    3. Under SMTP, you’ll see the credentials for this inbox.

    Add Mailtrap as an SMTP server in CraftForms:

    1. Go to CraftForms → SMTP Servers and click Add New.
    2. Fill in:
      • Name: Mailtrap Sandbox (or any label you’ll recognise)
      • Host: sandbox.smtp.mailtrap.io
      • Port: 2525
      • Username: your inbox username from Mailtrap
      • Password: your inbox password from Mailtrap
    3. Click Save.

    The server is now available to any email action in CraftForms. Go back to your form, open the Send Email Template action, and make sure Mailtrap Sandbox is selected as the SMTP server.

    Run the test:

    Fill in and submit the form as a real visitor would. Open Mailtrap’s dashboard and click into your inbox. Within a few seconds the test email appears. Click it to open the preview.

    Craftforms mailtrap email

    You can see exactly how the email renders, inspect every header, and check the spam analysis. If the layout needs adjusting, go back to the template, make changes, save, and submit the form again. Because Mailtrap catches every send, you can iterate freely without any risk.


    Step 6: Check the email log in CraftForms

    CraftForms logs every outgoing email regardless of which transport sent it. Go to CraftForms → Email Logs.

    Each row in the log shows:

    • Date — when the email was attempted
    • Transport — which server sent it (in this case, SMTP: Mailtrap Sandbox)
    • Status — a green “Success” badge or a red “Failed” badge
    • Actions — click to view the full email body and any error message
    Craftforms email log email

    If the send failed, the error message is recorded — wrong password, port blocked by the host’s firewall, rate limit exceeded. This is significantly faster to diagnose than chasing down a missing email with no trail.

    The log is a permanent audit trail of every notification your forms have sent. You can filter by status (all / success / failed), view the full HTML body of any email, and delete old entries when you’re done with them.


    Going live: connect a production SMTP server

    When the template looks right and test submissions are arriving cleanly in Mailtrap, the only remaining step is to switch the SMTP server in the Send Email Template action to your production sender.

    If you don’t have one set up yet, our guide to the best free SMTP services for WordPress covers the main options — including Brevo (easiest to set up, 300 emails/day free), Mailgun (best deliverability for bookings and high-value confirmations), Gmail SMTP, and others. Add whichever you choose under CraftForms → SMTP Servers, then update the action to use it instead of Mailtrap Sandbox.

    From that point on, every form submission sends the real branded email to the real recipient — through an authenticated SMTP server, with a delivery log you can actually read.


    Summary

    CraftForms’ email template builder gives you a complete WYSIWYG workflow inside WordPress:

    1. Design a branded template in the block editor — no HTML required
    2. Insert dynamic tags from the Dynamic Data panel to personalise every send
    3. Preview the compiled result before connecting it to anything
    4. Test safely with Mailtrap’s free sandbox — catch and inspect every test send in a browser inbox
    5. Confirm delivery in CraftForms → Email Logs — transport, status, and full body for every email
    6. Go live by connecting a production SMTP server when you’re ready to send to real recipients

    The difference between a plain-text notification and a properly designed confirmation email is the difference between a site that processes submissions and a business that communicates.

  • Best Free SMTP for WordPress — CraftForms Setup Guide

    Best Free SMTP for WordPress — CraftForms Setup Guide


    TL;DR

    • WordPress’s built-in PHP mail fails silently on most shared hosting — form notifications vanish with no error logged anywhere.
    • Before signing up for anything, use CraftForms’ built-in email test to confirm whether your server’s default mail actually works. You might not need SMTP at all.
    • If it fails: pick a free SMTP provider. Brevo is the easiest to set up (300 emails/day free). Mailgun gives the best deliverability for bookings and high-value confirmations.
    • Add it under CraftForms → SMTP Servers, assign it to your form’s email actions, confirm in the email log — about 10 minutes end to end.

    You built a contact form, tested it, watched the “Thank you” message appear — and assumed everything was working. Three weeks later a client mentions they never heard back. You check WordPress and the submission is there, sitting in the database. So where did the notification email go?

    This scenario is far more common than it should be. WordPress sends email using your web server’s built-in PHP mail function. On a properly configured dedicated server that works fine. On the shared hosting plans that most WordPress sites run on, it fails silently — emails leave WordPress, get rejected by recipient mail servers, land in spam, or simply disappear with no error logged anywhere.

    The fix is SMTP: a dedicated email service that handles authentication, delivery tracking, and spam reputation so your form notifications actually arrive. Most SMTP providers have a free tier that is more than enough for a small business or personal site.

    This guide covers the five best free options, how each one compares, and step-by-step instructions for connecting any of them to CraftForms.


    Why WordPress email breaks in the first place

    When WordPress calls wp_mail(), it hands the message to PHP’s built-in mail() function, which asks your web server to deliver it directly. This approach has three problems on shared hosting:

    No authentication. Spam filters on receiving servers (Gmail, Outlook, your client’s corporate mail) expect email to come from an authenticated sender. A bare php mail() call carries no authentication at all, so the receiving server has no way to verify the email is legitimate.

    No SPF or DKIM. These DNS records tell the world which servers are allowed to send mail for your domain. Shared hosting servers send mail for hundreds of domains simultaneously, so they can’t be listed in everyone’s SPF records. The result is a soft fail that often means spam or rejection.

    Silent failures. PHP’s mail() returns true when it hands the message to the local mail daemon — not when the message is delivered. If the daemon queues it and it later bounces, WordPress never finds out. You have no log, no error, and no idea.

    SMTP fixes all three. You authenticate with a dedicated service using a username and password or API key, the service is listed in your SPF/DKIM records, and it gives you a delivery log you can actually read.


    Check if your default WordPress email is working

    Before signing up for any SMTP service, it’s worth confirming whether you actually have a problem. On some hosts the built-in mailer works fine — and if it does, you don’t need to change anything.

    CraftForms has a built-in tool for exactly this. Go to CraftForms → SMTP Servers in your WordPress admin. At the top of the page you’ll see a panel titled “Check Your Default Email First.”

    Craftforms smtp servers manager
    CarftForms SMTP servers manager

    Enter an email address you can check (your own is fine) and click Send Test Email. CraftForms sends a message with a 6-digit code. If the email arrives, enter the code to confirm — the panel will show a green “Confirmed — email is working” badge. Your server’s default mailer is fine and you can stop here.

    If the email doesn’t arrive within a minute, or you see a “NOT OK — send failed” badge, your server’s built-in mail is broken. Continue to the provider comparison and setup steps below.


    The five best free SMTP services

    Here is a practical comparison of the services that work best with WordPress, ordered by ease of setup.


    1. Brevo (formerly Sendinblue) — best for beginners

    Free tier: 300 emails per day, unlimited contacts, no credit card required.

    Brevo is the easiest to set up. Sign up, verify your sending domain (a copy-paste DNS record), and you have SMTP credentials in under five minutes. The free plan is generous enough for most small business contact forms and booking notifications. The dashboard shows real-time delivery stats, bounces, and spam complaints.

    SMTP settings (values you will enter in CraftForms when adding your SMTP server — see Step 1 below):

    • Host: smtp-relay.brevo.com
    • Port: 587 (TLS)
    • Username: your Brevo login email
    • Password: your SMTP key from Brevo’s SMTP & API page (not your account password)

    Best for: Sites that want the quickest possible setup and don’t need more than ~9,000 emails per month.


    2. Mailgun — best deliverability

    Free tier: 100 emails per day on the Flex plan.

    Mailgun is what large SaaS products use for transactional email. Deliverability is excellent because Mailgun’s IP reputation is actively managed, and the logs are detailed — you can see exactly when a message was opened, bounced, or complained about. The setup is slightly more involved: you add DNS records to verify your domain before you can send.

    SMTP settings (values you will enter in CraftForms when adding your SMTP server — see Step 1 below):

    • Host: smtp.mailgun.org
    • Port: 587 (TLS)
    • Username: [email protected] (shown in Mailgun’s domain settings)
    • Password: your Mailgun SMTP password from the domain settings page

    Best for: Sites where deliverability matters most — high-value bookings, payment confirmations, order notifications.


    3. SendGrid — best for growth

    Free tier: 100 emails per day permanently, no expiry.

    SendGrid is owned by Twilio and has an enormous sending infrastructure. The free plan doesn’t expire, making it a solid long-term option. Domain authentication is required (DNS records), and SendGrid’s interface is more technical than Brevo’s, but the reliability and logs are excellent.

    SMTP settings (values you will enter in CraftForms when adding your SMTP server — see Step 1 below):

    • Host: smtp.sendgrid.net
    • Port: 587 (TLS)
    • Username: apikey (literally the string “apikey”)
    • Password: your SendGrid API key (generated in Settings → API Keys, with “Mail Send” permission)

    Best for: Sites likely to grow, where you want a provider with a clear upgrade path and no surprises.


    4. Gmail SMTP — free with any Google account

    Free tier: 500 emails per day (Google Workspace: 2,000/day).

    If you already have a Google account, you can use Gmail’s SMTP server without signing up for anything. The catch: Google no longer allows your regular password — you need to create an “App Password” in your Google account security settings, which requires 2-Step Verification to be enabled first.

    Using a personal @gmail.com address as your “from” address looks unprofessional for business forms. Gmail SMTP is best used with a Google Workspace account so you can send from [email protected].

    SMTP settings (values you will enter in CraftForms when adding your SMTP server — see Step 1 below):

    • Host: smtp.gmail.com
    • Port: 587 (TLS)
    • Username: your full Gmail address
    • Password: your App Password (16-character code from Google Account → Security → App Passwords)

    Best for: Sites already using Google Workspace, or personal projects where the volume is low and you don’t want to create another account.


    5. Amazon SES — cheapest at scale

    Free tier: $0.10 per 1,000 emails — effectively free at low volume, cheapest option once you exceed other free tiers.

    Amazon SES has the best price-to-deliverability ratio if you expect significant volume. The setup is the most technical — you need an AWS account, domain verification, and starting in the SES “sandbox” means you can only send to verified addresses until you request production access.

    SMTP settings (values you will enter in CraftForms when adding your SMTP server — see Step 1 below):

    • Host: email-smtp.[region].amazonaws.com (e.g. email-smtp.eu-west-1.amazonaws.com)
    • Port: 587 (TLS)
    • Username and password: generated SMTP credentials from AWS IAM (not your AWS login)

    Best for: Sites already using AWS infrastructure, or high-volume senders who have outgrown free tiers.


    6. Mailtrap — best for testing and staging

    Free tier: 1,000 test emails per month on the Email Testing plan; also 1,000 sends per month on the free Email Sending plan. No credit card required.

    Mailtrap is different from the other services on this list. Its signature feature is a virtual sandbox inbox: instead of delivering emails to real addresses, the sandbox intercepts them and displays them in a web interface where you can inspect the full HTML render, raw source, headers, and spam score. Nothing reaches a real inbox — which makes it the safest way to test a new form setup, a new email template, or a new SMTP configuration without any risk of sending half-finished emails to customers.

    The free Email Testing plan gives you up to five sandbox inboxes, each with its own SMTP credentials. Switch to these credentials in CraftForms while you’re building and testing; when everything looks right, swap in your production SMTP server (Brevo, Mailgun, etc.) with a single settings change.

    SMTP settings for the sandbox inbox (values you will enter in CraftForms when adding your SMTP server — see Step 1 below):

    • Host: sandbox.smtp.mailtrap.io
    • Port: 2525 (also accepts 587 and 465)
    • Username: the inbox username shown in Mailtrap under Inboxes → SMTP/POP3
    • Password: the inbox password shown alongside the username

    Best for: Anyone actively building or testing a form who wants to see exactly what the outgoing email looks like before it goes to a real recipient. Also useful for staging sites where the default WordPress mailer is disabled.


    Quick comparison

    ProviderFree emails/daySetup difficultyBest for
    Brevo300EasyQuick setup, beginners
    Mailgun100MediumBest deliverability, bookings
    SendGrid100MediumLong-term, scalable
    Gmail SMTP500Easy–mediumGoogle Workspace users
    Amazon SESLow costHardHigh volume, AWS users
    Mailtrap1,000 test/moEasyTesting, staging, template dev

    Connecting SMTP to CraftForms

    Step 1: Add your SMTP server in CraftForms

    Once you have credentials from your chosen provider, adding them to CraftForms takes about two minutes.

    1. Go to CraftForms → SMTP Servers.
    2. Click Add New.
    3. Fill in the fields in the dialog that opens:
    Sc
    Add new SMTP server in CraftForms
    • Name — a label for your own reference, e.g. “Brevo — main site”
    • Description — optional note about what this server is used for
    • Host — the SMTP hostname from your provider (e.g. smtp-relay.brevo.com)
    • Port — use 587 for TLS (recommended) or 465 for SSL. CraftForms detects the encryption type automatically from the port you enter.
    • Username and Password — from your provider’s SMTP settings page (see the per-provider sections above for the exact values)
    1. Click Save.

    The password is stored encrypted in your database using AES-256 — it is never stored in plain text.

    You can add as many SMTP servers as you like. A common setup is one server for important transactional emails (booking confirmations, payment receipts) and a separate one for lower-priority contact form alerts.


    Step 2: Use the SMTP server in a form’s email action

    By default, every Send Email and Send Email Template action in CraftForms uses your site’s default WordPress mailer. To route a specific email action through your new SMTP server:

    1. Open the form in the form editor.
    2. Click on the Send Email or Send Email Template action you want to change.
    3. Tick Use custom SMTP server and select the server from the dropdown.
    CraftForms Send Email action showing the 'Use custom SMTP server' checkbox ticked with a server dropdown below
    Sc
    Choose SMTP server for Send Email/Send Email Template submit actions
    1. Click Save.

    This is set per action, not per form — so you can have a booking confirmation routed through Mailgun for maximum reliability while a low-priority internal alert uses the default WordPress mail, all within the same form.


    Step 3: Check the email log after your next submission

    CraftForms logs every outgoing email, regardless of whether it was sent through SMTP or the default mailer.

    Go to CraftForms → Email Logs. Each row shows:

    • Recipient — who the email was sent to
    • Subject — the email subject line
    • Transport — which mailer sent it (e.g. “SMTP: Brevo — main site” or “WP mailer”)
    • Status — Success or Failed
    • Error message — if the send failed, the provider’s error reason is recorded here

    If you see a Failed entry, the error message tells you exactly what went wrong — wrong password, invalid hostname, port blocked by your host’s firewall, or rate limit exceeded. This makes diagnosis much faster than chasing down a missing email with no trail.

    You can filter the log by status and delete old entries. Entries moved to trash are automatically removed after 30 days.


    Summary

    For most WordPress sites with contact forms and booking notifications, Brevo is the right starting point — 300 emails per day for free, the easiest setup of any provider, and a generous free tier that suits the majority of small business sites.

    If you’re running a property rental, a service business taking bookings online, or any setup where a missed email costs real money, go with Mailgun. The extra DNS setup step is worth it for the deliverability.

    Either way, the CraftForms setup is the same: add the server under CraftForms → SMTP Servers, assign it to your email actions, and confirm delivery through CraftForms → Email Logs. Ten minutes of setup and you’ll never silently lose a form notification again.

  • Use WordPress as a Locked-Down Form Backend for Static Sites

    Use WordPress as a Locked-Down Form Backend for Static Sites

    Static sites are fast, cheap to host, and nearly impossible to compromise — but they can’t process a contact form. Every static site eventually hits the same wall: you need a backend.

    Most developers reach for a third-party service (Formspree, Netlify Forms, Basin) or bolt on a separate server. Both options add a dependency you don’t control, a recurring cost, and submission data stored on someone else’s infrastructure. There is a third option that gives you full ownership, unlimited forms, and a security profile close to zero: a locked-down WordPress installation used exclusively as a form backend.

    One WordPress install. Zero public pages. Every form submission from every static site you own — handled, stored, and routed — on infrastructure you control.

    This article is an evolution of Using WordPress as a Form Backend for Static Sites and Web Apps. That article introduced the idea — a single WordPress install as a submission endpoint. This one picks up where it left off: the site is locked down and invisible to visitors, improved security setup. CraftForms now supports embedded forms — the backend serves the form HTML directly to any external page, with no markup required on the static site side — and the full ecommerce and booking stack that comes with them. The same backend that took contact form submissions can now handle bookings, inventory on a site that has no server of its own.


    Part 1 — The Architecture: One Backend, Many Static Sites

    The Stack

    Three tools, each doing exactly one job:

    • WordPress — the backend. Locked down so aggressively it no longer resembles a normal WP install. No theme, no public content, no extra plugins.
    • CraftForms — the form engine. Handles form building, validation, submissions, conditional logic, file uploads, and email notifications.
    • Builderius — the optional static site builder. Design your pages visually and export clean HTML/CSS/JS files with no WordPress dependency in production.
    Static sites embed schema

    Your static sites connect to the WordPress backend over HTTPS. Static site A makes a direct fetch call on form submit. Static sites B and C use CraftForms’ embed feature — the form HTML is served from WordPress and rendered on the page automatically. Both methods hit the same craftforms/v1 REST endpoint; everything else on the WordPress install is locked down.

    What the locked-down WP install does NOT have

    • No public frontend — all page and post requests return 403
    • No theme vulnerabilities — no theme is active
    • No page builder, no WooCommerce, no third-party contact form plugin
    • No XML-RPC
    • No /wp-login.php at its default path

    A JAMstack site on Cloudflare Pages or Netlify serves your visitors. WordPress never touches a public HTTP request. It only processes form submissions.


    Part 2 — Locking Down the WordPress Installation

    Why One Plugin Changes Everything

    The most common vector for WordPress compromise is not your hosting provider — it’s outdated plugins. Every plugin in your install is a potential attack surface: a page builder you added for one client project, a contact form plugin with a stored XSS CVE published last week, a WooCommerce extension that stopped receiving updates.

    A WordPress installation with one plugin and a blocked public frontend has an attack surface close to zero. No theme vulnerabilities, no page builder vulnerabilities, no contact form plugin vulnerabilities — because none of those exist on this install.

    The steps below lock down the remaining standard entry points.


    Step 1 — Block the WordPress Frontend

    The template_redirect action fires before WordPress outputs anything. For any visitor who is not logged in, the hook returns a 403 and exits — no page, no post, no homepage is ever served. Because this runs in PHP it works on any server: Apache, nginx, or a local PHP built-in server. No .htaccess rules or server configuration required.

    template_redirect does not fire for REST API requests or wp-admin, so the CraftForms submission endpoint and the admin panel remain fully accessible to logged-in users and external form submissions.

    The implementation is in the complete mu-plugin below.


    Step 2 — Restrict the REST API to CraftForms Only

    All REST namespaces except craftforms/v1 return 403. This closes user enumeration (GET /wp-json/wp/v2/users), route discovery (GET /wp-json/), and every standard WordPress REST exploit in one filter. The filter fires after WordPress resolves the CORS OPTIONS preflight, so cross-origin submissions from your static sites continue to work correctly.

    Create wp-content/mu-plugins/craftforms-backend.php — files in mu-plugins/ load automatically on every request, no activation required. The full implementation is in the complete mu-plugin below.


    Step 3 — Hide the WordPress Login URL

    Automated brute-force scripts target /wp-login.php by default. Moving the login to an unpredictable URL removes your install from every automated scan. Pick a slug that is long, random, and only you know — and store it somewhere safe. Your login page will be at https://your-wp-backend.com/your-secret-slug. Losing the slug means you cannot log in.

    The full implementation is in the complete mu-plugin below.


    Complete mu-plugin

    Create a new PHP file craftforms-backend.php Drop this single file in wp-content/mu-plugins/ and all four measures are active immediately:

    <?php
    /**
     * CraftForms backend — security measures.
     * Place in: wp-content/mu-plugins/craftforms-backend.php
     */
    if ( ! defined( 'ABSPATH' ) ) exit;
    
    // ── 1. Restrict REST API to craftforms/v1 only ──────────────────────────────
    add_filter( 'rest_pre_dispatch', function ( $result, $server, $request ) {
        $route = $request->get_route();
        if ( strpos( $route, '/craftforms/' ) === 0 ) {
            return $result;
        }
        return new \WP_Error(
            'rest_restricted',
            'REST API is disabled on this installation.',
            [ 'status' => 403 ]
        );
    }, 10, 3 );
    
    // ── 2. Disable XML-RPC ───────────────────────────────────────────────────────
    add_filter( 'xmlrpc_enabled', '__return_false' );
    
    // ── 3. Custom login URL ──────────────────────────────────────────────────────
    if ( ! defined( 'CF_LOGIN_SLUG' ) ) {
        define( 'CF_LOGIN_SLUG', 'my-secret-access-8k2m9x' ); // ← CHANGE THIS
    }
    
    add_action( 'init', function () {
        global $pagenow;
        $request_path = parse_url( $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH );
    
        if ( $request_path === '/' . CF_LOGIN_SLUG ) {
            // wp-login.php reads $user_login and $error before conditionally setting
            // them; initialise here to prevent PHP 8 "Undefined variable" warnings.
            global $error;
            $error      = $error ?? null;
            $user_login = '';
            require_once ABSPATH . 'wp-login.php';
            exit;
        }
    
        if ( $pagenow === 'wp-login.php' ) {
            status_header( 404 );
            nocache_headers();
            exit( 'Not found.' );
        }
    } );
    
    // Rewrite site_url( 'wp-login.php', 'login|login_post' ) calls so the login
    // form action POSTs to the custom slug instead of the blocked wp-login.php.
    add_filter( 'site_url', function ( $url, $path, $scheme ) {
        if ( 'wp-login.php' === $path && in_array( $scheme, [ 'login', 'login_post' ], true ) ) {
            return home_url( CF_LOGIN_SLUG );
        }
        return $url;
    }, 10, 3 );
    
    add_filter( 'login_url', function ( $url, $redirect, $force_reauth ) {
        $custom = home_url( CF_LOGIN_SLUG );
        if ( $redirect ) {
            $custom = add_query_arg( 'redirect_to', urlencode( $redirect ), $custom );
        }
        return $custom;
    }, 10, 3 );
    
    add_filter( 'logout_url', function ( $url ) {
        return str_replace( 'wp-login.php', CF_LOGIN_SLUG, $url );
    } );
    
    // ── 4. Block all public frontend requests ───────────────────────────────────
    add_action( 'template_redirect', function () {
        if ( is_user_logged_in() ) {
            return;
        }
        status_header( 403 );
        nocache_headers();
        exit;
    } );
    

    Verify the setup

    # Should return 403
    curl https://your-wp-backend.com/wp-json/wp/v2/
    
    # Should return form data
    curl https://your-wp-backend.com/wp-json/craftforms/v1/embed/YOUR_KEY
    

    Nathan Foley has prepared a GIST – an updated version of this my MU plugin version. The comment was published in our FB group, you can check it here.


    Security Measures Summary

    MeasureWhat it blocks
    Minimal plugin countEvery plugin not installed = zero CVEs from that plugin
    PHP frontend blockWeb scrapers, bots, and crawlers requesting WordPress pages — works on any server without .htaccess
    REST API namespace restrictionUser enumeration (/wp/v2/users), route discovery, WordPress REST exploits
    XML-RPC disabledBrute-force via XML-RPC, pingback DDoS amplification
    Hidden login URLAutomated brute-force scripts targeting /wp-login.php

    Part 3 — CraftForms: External Submissions and Embedding

    Enable External Submissions

    Open any CraftForms form in the WordPress editor. Scroll to the Advanced Settings panel at the bottom of the settings sidebar — it shows the form’s submission URL and a button to open the full configuration.

    Advanced settings
    Advanced Settings on the form edit page

    Click Configure Submission Settings. The modal opens with everything you need: the toggle, the endpoint URL, a ready-to-run cURL example, and the field validation table.

    Advanced settings modal
    Submission Settings

    Toggle Allow External Submissions on. The API Reference section below it shows the endpoint — the form’s REST name (a human-readable slug you set when creating the form, e.g. contact-form):

    POST https://your-wp-backend.com/wp-json/craftforms/v1/submit/contact-form
    

    Submitting from Your Static Site

    CraftForms accepts application/json. For most static site integrations this is the cleanest approach:

    cURL:

    curl -X POST "https://your-wp-backend.com/wp-json/craftforms/v1/submit/contact-form" \
      -H "Content-Type: application/json" \
      -d '{"name":"John Doe","email":"[email protected]","message":"Hello from curl"}'
    

    Vanilla JavaScript:

    <form id="contact-form">
      <input name="name" type="text" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message"></textarea>
      <button type="submit">Send</button>
      <p id="status"></p>
    </form>
    
    <script>
    document.getElementById('contact-form').addEventListener('submit', async (e) => {
      e.preventDefault();
      const status = document.getElementById('status');
      status.textContent = 'Sending…';
    
      const data = Object.fromEntries(new FormData(e.target));
    
      const res = await fetch(
        'https://your-wp-backend.com/wp-json/craftforms/v1/submit/contact-form',
        {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(data),
        }
      );
    
      const json = await res.json();
      if (json.success) {
        status.textContent = json.data.successMsg || 'Sent!';
        e.target.reset();
      } else {
        status.textContent = json.data.errorMsg || 'Something went wrong.';
      }
    });
    </script>
    

    Field names must match the Name attribute of each CraftForms field block. The Field Validation table in the Submission Settings modal shows the exact field names and their validation rules:

    Advanced settings field valdiation
    Field validation schema

    Required Request Headers

    For an extra layer of spam protection, require a shared secret on every submission. Open the Submission Settings modal and click + Add required header in the Required Request Headers section. Any request that omits the header — or sends the wrong value — is rejected before the form is processed.

    The header name is entirely up to you — X-CF-Token is just one example. Pick any name and any value:

    Header nameHeader value
    X-CF-Tokenyour-secret-value

    Add the header to every fetch call from your static site:

    const res = await fetch(
      'https://your-wp-backend.com/wp-json/craftforms/v1/submit/contact-form',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CF-Token': 'your-secret-value',
        },
        body: JSON.stringify(data),
      }
    );
    

    Note: This header is visible in the browser’s DevTools Network panel, so it is not a true secret for public-facing forms. It raises the bar for automated spam — scripts that don’t know the header will be rejected — but it is not a substitute for rate limiting. For server-to-server calls (a serverless function proxying the submission) it acts as a proper shared secret.


    Embedding a Form

    With embedding you don’t build a form on the external site at all. CraftForms renders the form HTML and delivers it — along with all required styles and scripts — directly to the external page. The form submits back to the same WordPress backend automatically.

    Setup:

    1. Make sure Allow External Submissions is enabled on the form (step above).
    2. Go to CraftForms → Settings → Embed tab. Click Generate new key, select the form, and enter the external domain (e.g. mysite.com — no protocol, no trailing slash).
    Settings embed
    Add embed key
    1. Click the Snippet button on the new row.
    Settings embed modal
    Embed snippet

    Copy the snippet and paste it anywhere in your HTML page:

    <div
      data-craftforms-embed="aHR0cHM6Ly9zYW5kYm94LnRlc3Q.oHrv6dPVreM">
    </div>
    <script
      src="https://your-wp-backend.com/wp-content/plugins/craftforms/build/webcomponents/embed.js"
      defer>
    </script>
    

    The <div> is replaced by the live form at page load — no configuration on the static site side, no build step, no manual form markup.

    What you get for free with an embedded form:

    • Every CraftForms field type — text, email, file uploads, dropdowns, date pickers, conditional fields, multi-step flows.
    • Advanced components — star ratings, range sliders, repeater groups, catalog selectors.
    • Client-side validation built in — required fields, email format, character limits, custom error messages. Everything works out of the box; you write zero validation JavaScript.
    • Consistent UI — the form looks and behaves identically everywhere it is embedded, because it is the same rendered output from the same source.

    Compare that to building the form manually: custom markup, a validation library, wiring up field names, handling error states, writing the fetch call, testing cross-browser. With embedding, that work is already done.

    All form submissions use the ?rest_route= URL format — fully compatible with the locked-down setup in Part 2, even if /wp-json/ is blocked at the server level.

    With a catalog resource (booking, product, etc.):

    <div
      data-craftforms-embed="aHR0cHM6Ly9zYW5kYm94LnRlc3Q.oHrv6dPVreM"
      data-resource-id="42">
    </div>
    <script src="…/embed.js" defer></script>
    

    What Your Static Site Gains

    Third-party form services (Formspree, Netlify Forms, Basin) give you one thing: an email on form submit. CraftForms gives you a backend.

    Email that doesn’t land in spam

    CraftForms routes email through a real SMTP provider — not PHP’s wp_mail. Under SMTP Servers in the admin menu, add as many providers as you need — Postmark, SendGrid, Mailgun, your own mail server — each with its own credentials. Then, inside each form’s submit actions, the Send Email and Send Email Template actions each have an SMTP server selector: pick which provider handles that delivery. A contact form can send notifications via Postmark; a booking form can use a separate provider tied to your reservations mailbox. No shared infrastructure, no sender reputation you don’t control.

    Branded HTML email templates — designed in WordPress

    The email template designer is a Gutenberg editor. Add headings, images, buttons, and text blocks; insert {{field_name}} variables anywhere. CraftForms compiles the design to optimised, email-client-compatible HTML automatically. The confirmation email a customer gets after booking a stay looks like it came from a real hospitality brand — because you designed it, in the same editor you use for everything else.

    With an embedded form, your static site gets ecommerce and booking

    This is where the gap between a third-party service and CraftForms widens the most. An embedded CraftForms form isn’t just a contact form — it can be any form type the plugin supports, and Pro forms include:

    • File uploads — with server-side MIME validation and automatic import into the WordPress Media Library
    • Price calculator — Smart Variables evaluate a formula as the user selects options; the live price updates in real time before they submit, and the server recalculates on submission so the charged amount can never be manipulated client-side
    • Booking datepicker — hotel-style checkin/checkout ranges, fixed time-slot grids, or single-date selection; blocked dates and advance-notice requirements enforced visually
    • Catalog and inventory — attach a resource to a form; availability is tracked per date and per slot automatically; pre-submission stock checks prevent double-booking
    • iCal sync — paste an Airbnb or Booking.com iCal URL and those dates are marked unavailable in your datepicker automatically; a private .ics feed goes back the other direction so external platforms stay in sync

    A static site on Cloudflare Pages with a CraftForms embedded form can take bookings, calculate and charge prices, manage inventory, and send a branded confirmation email — all without a server of its own, and without stitching together five separate services.


    Part 4 — Builderius: Build the Static Frontend Without Code

    Builderius is a visual site builder that runs inside WordPress. Design your pages with drag-and-drop, then export the result as pure HTML, CSS, and JavaScript — no PHP, no database, no WordPress dependency in production.

    Workflow:

    1. Build your site in Builderius — on a local or staging WordPress install, completely separate from the locked-down form backend.
    2. Export the static build. The output is clean HTML files and assets. No server-side code, no theme files, nothing to maintain.
    3. Deploy to Cloudflare Pages (free tier, globally distributed CDN, deploys from a git push in seconds) or GitHub pages. Or download the static site as ZIP archive and deploy anywhere you want.
    4. Connect to CraftForms:
      • Option A — Embed: paste the CraftForms snippet into your exported HTML. The form renders automatically on page load. Zero custom JavaScript required.
      • Option B — Custom fetch: build your form directly in Builderius — its form builder lets you design a fully styled form with complete control over every element and its markup. Submit the form data to the endpoint URL provided by CraftForms. You own the design; CraftForms handles the processing.

    The result: a static CDN site with millisecond load times and a tiny security surface on the frontend. The WordPress backend handles form processing and storage — it never serves a single page to a visitor.


    All features described in this article — external submissions, required headers, and form embedding — are part of CraftForms PRO.