Most Contact Form 7 examples on the web are a name field, an email field and a message box. That markup was already in the plugin when it was installed. The examples that are actually needed are the ones for a form that has to do a job: take an application with a file attached, route a request to the right person without publishing their address, or hold a signup that has a cutoff.
The markup below is built from the form-tag options documented by the plugin, with the settings that most often break these forms called out where they belong. Contact Form 7 currently reports 10 million or more active installations and lists WordPress 6.7 or higher and PHP 7.4 or higher as requirements, so almost none of this is exotic. The mistakes are just quiet.
Read the convention before copying the markup
Two conventions explain most of the syntax. An asterisk marks a required field, so text accepts anything and text* refuses to submit while empty. The same applies to email*, tel*, url*, file*, checkbox* and select*. A radio button group is naturally required, so radio has no starred variant, and the documentation advises preselecting an option in a radio group with the default:1 option.
The second convention is that the first token after the tag name is the field name, and everything after it is an option or a value. The value in quotes becomes the default content of the field, not the label.
[text* your-name placeholder "Full name"]
[email* your-email placeholder "[email protected]"]
[tel your-phone minlength:10 maxlength:20]
[textarea your-message maxlength:2000 placeholder "What is this about"]
[submit "Send"]
Two options are worth setting explicitly on every text field. The maxlength: option has a documented default of 400 characters, which is short enough to silently truncate a long message body if it is left alone on a textarea. The placeholder option, for which watermark works as an alias, makes the quoted value appear as placeholder text instead of as a pre-filled default value, which matters because a pre-filled default gets submitted verbatim by people who do not clear it.
Autocompletion is the third. Adding autocomplete:name to a name field and autocomplete:email to an email field lets the browser fill them, which measurably reduces typos in the one field where a typo costs a reply.
An application intake form with a file
An application form has two parts that a contact form does not: an attachment, and a routing decision.
[text* applicant-name autocomplete:name]
[email* applicant-email autocomplete:email]
[select* applying-for include_blank "Editorial|[email protected]" "Design|[email protected]" "Operations|[email protected]"]
[file* applicant-cv filetypes:pdf|doc|docx limit:5mb]
[textarea applicant-note maxlength:1500]
[submit "Apply"]
The filetypes: and limit: options are the two that need to be written out. When they are omitted, the documentation states that the default filetypes: value is audio/*|video/*|image/* and the default limit: value is 1mb, and it recommends setting both explicitly because those defaults can change between versions. An applicant sending a 3 MB PDF into a field left at defaults gets an error that mentions neither the size nor the type. Note also that the limit cannot take a decimal point, so limit:1.5mb is ignored rather than honoured.
There is a ceiling above the field limit. The plugin documents an upper limit of 25 MB on the total size of files attached to one email, and it warns in the configuration screen when a form allows attachments that could exceed it. Any intake that involves portfolios runs into this, and the answer is a link to a shared folder rather than a larger limit.
The pipe character in the select values is the part most examples get wrong. Writing the raw addresses as option values puts them in the page source for anyone to scrape. With a pipe, only the part before it is exposed, and the part after it is used for mail replacement. The mail-tag [applying-for] then yields the address for the To field, while [_raw_applying-for] yields the visible label for the message body. The pipe works in drop-down menus, radio buttons and checkboxes, and can be disabled site-wide by defining the WPCF7_USE_PIPE constant as false.
A signup form with choices that stay readable
Event and course signups are mostly selection fields, and the options that make them usable are not the obvious ones.
[select* session include_blank "Morning" "Afternoon" "Evening"]
[radio attendance label_first default:1 "In person" "Online"]
[checkbox dietary use_label_element "Vegetarian" "No pork" "No alcohol"]
[checkbox* confirm-terms use_label_element "The cancellation policy has been read"]
[submit "Register"]
include_blank inserts a blank item at the top of a drop-down menu, which is what stops the first option from being submitted by everyone who never touched the field. use_label_element wraps each checkbox and radio button in a label tag, so the text becomes clickable rather than a target the width of the box itself. label_first reverses the default order and puts the label before the control.
Two more options solve problems that otherwise turn into support email. exclusive makes a checkbox group behave as a zero or one selection, which is useful when a group has to stay a group visually but accept only one answer, though the documentation notes it is a JavaScript behaviour and does nothing without JavaScript. free_text appends a free input text field to the last item, which is the standard way to offer an other option without a second field.
The Mail tab breaks more forms than the markup does
A form can be perfect and still send nothing useful, because the form template and the mail template are different things. In the Mail tab, [applicant-name] is a mail-tag that refers to the field; the whole [text* applicant-name] string is a form-tag and does not belong there. The File attachments field takes mail-tags only, so it takes [applicant-cv], never the form-tag that created the field.
Four settings in that tab are worth checking on every form.
The From field has to be an address on the same domain as the site, or the plugin raises a configuration error. Putting the submitter's address there is the habit that causes it, and the correct place for the submitter's address is a Reply-To line in Additional headers.
The checkbox labelled Exclude lines with blank mail-tags from output removes lines whose mail-tags are empty, which is what keeps optional fields from leaving a column of empty labels in every message. The Use HTML content type checkbox switches the body from plain text, and is worth leaving off unless the template genuinely needs markup.
Mail (2) is the second template, commonly used as the automatic reply to the submitter. The documentation is specific that Mail (2) is sent only when the primary Mail has been sent successfully, which means a broken primary template silently takes the applicant's confirmation with it.
Special mail-tags fill in the context that no field captures: [_remote_ip] for the submitter's IP address, [_url] for the page holding the form, [_date] and [_time] following the site's format settings, and [_serial_number], which increments per submission and requires Flamingo 1.5 or later. On a form that handles more than a handful of submissions, the serial number is the difference between referring to a case and describing it.
Test the form the way a visitor meets it
None of the markup above is finished until it has been submitted once from a logged out browser, on a phone, with a real address at a large mail provider. Testing while logged in as an administrator hides three things at once: the default values that only appear for logged-in users, the spam modules that treat a known session differently, and a From address that the receiving server quietly files as junk.
Submit the form once for each branch of any routing field, then check that the correct recipient received it and that the automatic reply arrived. Five minutes of this finds more than an hour of reading the template, because the failures are almost always in the delivery rather than in the tags.
Spam filtering without losing real submissions
Contact Form 7 provides several spam modules, and the documentation recommends using more than one together rather than relying on any single one.
Akismet is wired in through field options rather than a separate tag: akismet:author on the name field, akismet:author_email on the email field and akismet:author_url on a website field. When a submission is judged spam, the mail is suspended and the visitor sees a message saying there was an error trying to send the message, surrounded by an orange border. The documented test strings are viagra-test-123 in the author field and [email protected] in the email field, which is the fastest way to confirm the wiring works before trusting it.
reCAPTCHA in version 5.1 and later is v3 only, works in the background, and needs no tag at all. Any [recaptcha] tags left in a template are ignored and replaced by an empty string. The score threshold defaults to 0.50 and can be adjusted with the wpcf7_recaptcha_threshold filter. Turnstile is the other integration, placed at the top of the form by default and movable with a [turnstile] tag that accepts options including size, theme and language. The plugin's own documentation states that Turnstile, unlike Google reCAPTCHA, is available free of charge.
The disallowed list is the blunt instrument, and it lives outside the plugin. Words and IP addresses entered in the Disallowed Comment Keys box under Settings, then Discussion, cause matching submissions to be treated as spam. Pairing it with [_remote_ip] in the message body is how the address to block gets found in the first place.
The example nobody publishes: where the responses go
Here is the sentence that changes the shape of every form above, stated by the plugin's own documentation: Contact Form 7 does not store submitted messages anywhere. Nothing in the markup changes that. The mail is the record, and a mail server problem or a mistyped To field means the submission never existed.
Flamingo, from the same author, saves messages through contact forms into the database, and is where the reCAPTCHA score for each submission can be seen. Its Subject and From columns read from the default field names your-subject, your-name and your-email, so a form using different names needs mapping lines in the Additional Settings tab.
flamingo_email: "[applicant-email]"
flamingo_name: "[applicant-name]"
flamingo_subject: "[applying-for]"
do_not_store: true
That last line is the opposite switch, suppressing storage for a specific form when the submissions should not be kept.
| The job | What Contact Form 7 gives | What is still missing |
|---|---|---|
| Collecting structured input | Form-tags with validation, required fields, file limits | Nothing. This is the strong part |
| Getting it to the right person | Pipes in select and radio values, Additional headers | No record of who picked it up |
| Keeping the record | Nothing by default, Flamingo once installed | Owner and status per response |
| Replying | Mail (2) as an autoresponder | Threaded replies and a second reply check |
Storage plus routing plus a reply history is where a plugin stack ends and a tool with response management begins. The distinction is not about the form; it is about whether a response can carry an owner and a status, which the features page sets out, and whether that arrangement fits the intake being run, which the use cases page compares.
What to change first
Open one live form, set maxlength: and limit: explicitly, and confirm that the From address belongs to the site domain. Then answer the harder question of where the last hundred submissions are kept, and if the answer is a mailbox, put storage in place before the next campaign. Halict shows the same intake with an owner and a status on every response.
Q1. Where are Contact Form 7 submissions saved?
Nowhere, by default. The plugin's documentation states plainly that it does not store submitted messages, so the email it sends is the only copy. Installing Flamingo, which is free and by the same author, saves messages into the WordPress database from that point forward, but it cannot recover submissions that arrived earlier.
Q2. How can a Contact Form 7 form send to different addresses based on a choice?
Use a pipe in the option values, such as "Sales|[email protected]", and put the matching mail-tag in the To field of the Mail tab. Only the label before the pipe appears in the page source, which keeps the addresses out of reach of scrapers, and [_raw_fieldname] retrieves the visible label for the message body.
Q3. Why is an uploaded file rejected when it looks small enough?
Because the field is probably still on its defaults. Without explicit options, the documented default acceptable types are audio/*|video/*|image/* and the default size limit is 1mb, so a small PDF fails on type rather than size. Setting filetypes: and limit: explicitly on the tag fixes it, and the limit value cannot contain a decimal point.
Q4. Does the automatic reply still go out if the main email fails?
No. Mail (2), the template usually used as the autoresponder, is sent only after the primary Mail has been sent successfully. A configuration error in the primary template therefore stops both messages, which is why the submitter sometimes hears nothing at all.
Q5. Is reCAPTCHA v2 still an option in Contact Form 7?
Not in the core plugin. Version 5.1 and later support reCAPTCHA v3 only, and any leftover [recaptcha] tag is ignored and replaced with an empty string. Teams that need a visible checkbox use a third party add-on, and the other supported route is Turnstile, which the plugin documents as free to use.