Skip to main content

Scoped Duplicate CheckingProNew in 5.17+

Use the same form for multiple products, articles, or events while allowing each visitor to submit only once for each item. For example, a visitor can rate Product A and Product B, but cannot rate Product A twice.

The Duplicate Check setting determines how Freeform recognizes the visitor. Scope by Field determines which item they are submitting for. Leaving the scope blank keeps the existing form-wide restriction.

​Setup

1

Add a Hidden field with the handle productId, or another supported field that identifies the item.

2

Open the form's Settings → Limits and choose a once-only Duplicate Check option.

3

Select your field under Scope by Field.

4

Keep Store Submitted Data for this Form enabled under Settings → Data Storage. If storage requires the visitor to opt in, they must opt in to submit this form.

5

Populate the scope field with the item's identifier when rendering or submitting the form.

Duplicate CheckWith a scope field selected
Logged-in users — once per formOne submission per item for each logged-in user. Login is required.
Anyone — once per emailOne submission per item for each matching email address, checked across the form's Email fields. At least one nonempty email address is required.
Anyone — once per user or cookieOne submission per item, checked against the logged-in user or browser cookie.
Anyone — once per user, IP, or cookieThe same checks, plus the IP address when Collect IP Addresses is enabled.

Scope is available for these four once-only modes. No restrictions and Logged-in users — unlimited do not use a scope field.

​Twig Examples

These examples use a form with the handle productRating and a Hidden field with the handle productId. Enable AJAX when displaying multiple instances on one page.

On a Craft Entry page, pass the entry ID into values before checking form.duplicate:

{% set form = freeform.form('productRating', {
values: { productId: entry.id },
}) %}

{% if form %}
{% if form.duplicate %}
<p>You have already rated this product.</p>
{% else %}
{{ form.render() }}
{% endif %}
{% endif %}

Hiding the form is optional. Freeform validates the scope and rejects duplicate submissions on the server, including AJAX, REST, and GraphQL submissions. An email-based check can only identify a duplicate once the email address is available.

To also attach submissions to a Craft Entry's Freeform Submissions field, use the relations parameter. Relations and duplicate scope are configured separately.

​Supported Fields and Values

The scope must be a top-level field that stores a single value: Hidden, Invisible, Text, Textarea, Number, Range Slider, Email, Phone, Website, Regex, Dropdown, Radios, Rating, or Opinion Scale. Nested fields, Table columns, and fields containing multiple values are excluded.

  • A nonempty scope value is required when submitting. 0 is valid.
  • On multi-page forms, a scope field on a later page is required when that page is submitted, and always at final submission.
  • Text values match exactly, including case and whitespace. Numeric fields use their normalized values. Encrypted scope and Email fields are supported.
  • Spam and deleted submissions do not count. Editing an existing submission retains the existing editing behavior.
  • The check depends on stored submissions. Deleting or purging a matching submission removes it from future scoped checks.

The scope checks whether a value has already been submitted; it does not verify that the product exists or that the visitor may rate it. A Hidden field can be changed by the visitor. Validate item identifiers on the server using your site's rules. Use a Number field or server-side normalization if 1 and 01 should identify the same item.

​Browser Cookies and Headless Forms

Cookie-based modes use one signed, HTTP-only cookie per form, named freeform_voter_{formId}, with a one-year lifetime. Freeform stores a hash of the visitor identifier with the submission. It does not create a separate cookie for every product.

Headless clients must retain cookies between loading and submitting the form. For cross-origin clients, configure permitted origins, credentials, and cookie settings. See the REST API and GraphQL references.

Clearing cookies or using another browser allows another guest submission. For a limit tied to an account, use Logged-in users — once per form with a scope selected.

​Updating Existing Forms

Run Freeform's pending migrations, such as php craft up, before using this feature. Freeform 5.17 adds a nullable duplicateCheckIdentity column and a form/identity index to the existing submissions table; it does not create a new table.

Existing submissions can be matched by user, email, or IP when they contain the same scope value. Guest browser tracking starts with scoped submissions: old form-wide cookies cannot identify which items a guest submitted for.

Freeform rechecks for duplicates immediately before saving and uses Craft's mutex to coordinate simultaneous submissions. On installations with multiple application nodes, configure a shared mutex backend.