If you’ve gotten familiar with Craft’s control panel, you might be wondering what kinds of things you can do from your site’s front-end. The answer is actually quite a bit!

Any time you want to get information from (or send information to) Craft, it’s part of the request-response lifecycle. This page covers the process of setting up HTML forms (and other types of requests like Ajax!) to send and receive data in a way that Craft understands.

Controller Actions Reference
See a list of controller actions you can interact with via forms.

# Making Requests

Most of your interactions with Craft so far have likely been over HTTP. Let’s take a look at how Craft determines what to do when it receives a request.

# Params

This page will make frequent references to query and body params. These are named values sent with a request, either encoded after a ? in the URL, or within the “body” of a request. Query params are used in GET and POST requests, but body params are only used in POST requests.

Craft uses a handful of params to properly route a request, but may expect others based on what it thinks the user is requesting. For example, most requests use an action param, but a login request also requires a username and password param.

# Actions

An “action request” is one that explicitly declares the controller and action to use, via an action query or body param.

This action parameter is different from the <form action="..."> attribute:

  • The action param should be used within a URL query string (?action=...) for GET requests, or in the body of a POST request.
  • The form attribute should only be used when you want to control where a user is sent in failure scenarios. When an action param is not present in the request, you can use an “action path” like action="/actions/users/login". This attribute has no effect if a redirect is issued in response to the request!

Craft also supports routing to specific actions using a path (beginning with the actionTrigger setting), or by creating an rule in routes.php.

# HTTP Verbs

Each action usually responds to one HTTP method (opens new window). Using an unsupported method will throw a yii\web\BadRequestHttpException (opens new window), and show your error template with a 400 statusCode—or send a JSON response with an error key.


All POST requests are made through forms or Ajax, and require an action parameter and CSRF token.


{# Let your users request a password reset: #}
<form method="post">
  {{ csrfInput() }}
  {{ actionInput('users/send-password-reset-email') }}

  <label for="loginName">Username or email</label>
  {{ input('text', 'loginName', null, {
    id: 'loginName',
  }) }}

  <button>Reset Password</button>

Some POST requests will write flashes into the session to communicate successes and failures.

Flashes are not set when using Ajax. Look for confirmation and errors in the response!


GET requests are made by accessing an action URL by way of a regular anchor tag, a form submission, or Ajax. In the examples that follow, GET action requests are much less common than POST, as the bulk of read-only request routing is handled for you, out of the box.

{# Output a “log out” link: #}
<a href="{{ actionUrl('users/logout') }}">Log Out</a>

{# Craft actually provides a shortcut for this: #}
<a href="{{ logoutUrl }}">Log Out</a>

You may notice that the actionUrl() function generates URLs with index.php visible, despite your omitScriptNameInUrls setting. This is intended, as it guarantees compatibility with all environments, regardless of configuration.

If you need a cleaner URL, consider setting up a custom route.


Craft has built-in Cross-Site Request Forgery (opens new window) mitigation, and therefore requires a valid session and token any time data is POSTed to a controller action.

For requests initiated by an HTML <form>, use the csrfInput() Twig helper:


<form method="post">
  {{ csrfInput() }}
  {{ actionInput('entries/save-entry') }}

  {# ... #}

The process is slightly more complicated for Ajax requests, but can be abstracted in a manner appropriate for your project.

# Form Helpers

Craft has a number of built-in Twig functions to make dealing with forms and input easier.

Function Notes
actionInput() Generate a hidden HTML <input> element for controlling which action a <form> should route to.
actionUrl() Generate an absolute URL to the specified action, with any extra params. GET only.
csrfInput() Generate a hidden HTML <input> required for CSRF protection.
failMessageInput() Override error-condition flash messages. POST only, ignored for Ajax requests.
hiddenInput() Lower-level helper for generating hidden HTML inputs.
input() Even finer-grained control over HTML <input> element creation.
redirectInput() Generates a hidden HTML <input> element to control redirection after successful requests. POST only, ignored for Ajax requests.
successMessageInput() Override success-condition flash messages. POST only, ignored for Ajax requests.

# Ajax

In order to respond appropriately, Craft requires that Ajax requests are identified as such. Some tools (like jQuery) need no configuration; others (like the native fetch() API (opens new window)) will need to be configured explicitly:

Header Notes
Accept Set to application/json to receive a JSON response (when available).
X-Requested-With Set to XMLHttpRequest if your templates rely on
X-CSRF-Token Send a valid CSRF token (for POST requests) if none is provided in the request body under the key determined by csrfTokenName.
Content-Type Set to application/json if the request’s body is a serialized JSON payload (as opposed to FormData).

A CSRF token is still required for Ajax requests using the POST method. You can ensure a session is started (for guests) by preflighting a request to the users/session-info action:

// Helper for fetching a CSRF token:
const getSessionInfo = function() {
  return fetch('/actions/users/session-info', {
    headers: {
      'Accept': 'application/json',
  .then(response => response.json());

// Session info is passed to the chained handler:
  .then(session => {
    const params = new FormData();

    // Read the User’s ID from the session data (assuming they’re logged in):
    params.append('fullName', 'Tony Tiger');

    return fetch('/actions/users/save-user', {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'X-CSRF-Token': session.csrfTokenValue,
        'X-Requested-With': 'XMLHttpRequest',
      body: params,
    .then(response => response.json())
    .then(result => console.log(result));

This example assumes you have no preexisting HTML from the server, as though it were part of a headless application. If you are working on a hybrid front-end (and sprinkling interactivity into primarily server-rendered pages), you could eliminate the first request by stashing the user ID and CSRF token in the document’s <head> (or on another relevant element) and reading it with JavaScript:




  data-user-id="{{ }}"
  data-csrf-token-value="{{ }}"
  data-csrf-token-name="{{ }}">Edit Name</button>

  const $button = document.getElementById('update-name');

  $button.addEventListener('click', function(e) {
    const params = new FormData();

    params.append('userId', $button.dataset.userId);
    params.append($button.dataset.csrfTokenName, $button.dataset.csrfTokenValue);
    params.append('fullName', prompt('New name:'));

    fetch('/actions/users/save-user', {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'X-Requested-With': 'XMLHttpRequest',
      body: params,
    .then(response => response.json())
    .then(result => console.log(result));

# Sending JSON

If you prefer to work with a JSON payload for the body, you must include the appropriate Content-Type header (opens new window). The equivalent users/save-user request would look like this:



// ...
const params = {
  userId: $button.dataset.userId,
  fullName: prompt('New name:'),

fetch('/actions/users/save-user', {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'X-CSRF-Token': $button.dataset.csrfTokenValue,
    'X-Requested-With': 'XMLHttpRequest',
  body: JSON.stringify(params),
.then(response => response.json())
.then(result => console.log(result));

Files cannot be uploaded when using Content-Type: application/json.

When sending a JSON payload in the body of a request, you must use an action path (/actions/users/save-user, as in the example above), or provide the action in a query parameter (/index.php?action=users/save-user)—the action will not be properly picked up as a property of the decoded payload.

# Models and Validation

Most of the data creation and manipulation actions we’ll cover revolve around yii\base\Model (opens new window)s. Craft uses models to store and validate all kinds of things—including every type of element you’re already familiar with!

If you encounter errors when creating or saving something, it will usually be passed back to your template as a special variable like entry or user, and a flash will be set. Every model has a .getErrors() method that returns a list of messages for any attribute (or custom field) that did not validate.

While abbreviated, this “user profile” form contains all the patterns required to display contextual validation errors:

{% extends '_layouts/default' %}

{# Require an active user session: #}
{% requireLogin %}

{% block content %}
  {# Display the user’s saved name: #}
  <h1>Hello, {{ currentUser.fullName }}</h1>

  {# Normalize the `user` variable, so we can use it in the form regardless of whether or not it was rendered following a submission attempt: #}
  {% set user = user ?? currentUser %}

  <form method="post">
    {{ csrfInput() }}
    {{ actionInput('users/save-user') }}
    {{ hiddenInput('userId', }}

    <label for="fullName">Full Name</label>

    {{ input('text', 'fullName', user.fullName, {
      id: 'fullName',
      aria: {
        invalid: user.hasErrors('fullName'),
        errormessage: user.hasErrors('fullName') ? 'fullName-errors' : null,
    }) }}

    {% if user.hasErrors('fullName') %}
      <ul id="fullName-errors">
        {% for error in user.getErrors('fullName') %}
          <li>{{ error }}</li>
        {% endfor %}
    {% endif %}

    {# ...other fields and attributes... #}

{% endblock %}

The same principles apply to anything else you want to make editable in the front-end, so long as the user has the correct permissions. Take a look at the public registration forms (opens new window) for some examples of validation on forms available to guests—and to learn about some nice abstractions that will help reduce repetition in your form markup!

# Flashes

Flashes are temporary messages Craft stores in your session, typically under keys corresponding to their severity, like notice or error. You can output flashes in your templates:

{# Retrieve + clear all flashes: #}
{% set flashes = %}

{% if flashes | length %}
  {% for level, flash in flashes %}
    {# Use the level (most often `notice` or `error`) to customize styles: #}
    <p class="{{ level }}">{{ flash }}</p>
  {% endfor %}
{% endif %}

# Responses

Action requests are largely consistent in their behavior—exceptions will be noted in each of the available actionsResponse sections.

Let’s look at some typical success and failure states and how they differ.

# Success

Successful responses are mostly handled via the craft\web\Controller::asModelSuccess() (opens new window) or asSuccess() (opens new window) methods.

# After a GET Request

Craft’s response to a GET request varies based on whether it included an Accept: application/json header—and the substance of the response will differ greatly from action to action.

# After a POST Request

Successful POST requests will often culminate in a flash being set (under the notice key) and a 300-level redirection.

Some routes make this redirection configurable (passwordSuccessPath or activateAccountSuccessPath, for instance)—but sending a hashed redirect param with your request will always take precedence.

The redirectInput() function takes the guesswork out of rendering this input.


<form method="post">
  {{ csrfInput() }}
  {{ actionInput('users/send-password-reset-email') }}

  {# Redirect to a page with further instructions: #}
  {{ redirectInput('help/account-recovery') }}

  {# The above is equivalent to: #}
    value="{{ 'help/account-recovery' | hash }}">

  {{ input('email', 'loginName', null, {
    required: true,
  }) }}

  <button>Reset Password</button>

The redirect param accepts an object template, which is evaluated just before it’s issued, and can reference properties of the element or record you were working with:

<form method="post">
  {{ csrfInput() }}
  {{ actionInput('entries/save-entry') }}

  {# Redirect the user to the public page: #}
  {{ redirectInput('community-posts/{uid}') }}

  {# ...entry options... #}

  {{ input('text', 'title') }}

  <button>Post + View</button>

Inspecting the HTML output, you’ll see your template exactly as provided. Why wasn’t it rendered? The “template” will be securely submitted along with your POST request and be rendered after the entry is saved—that’s why we’re able to use properties (like {uid}, in the example) whose values aren’t yet known.

For JSON responses, redirection does’t make as much sense—so Craft will include the resolved redirect value for your client to navigate programmatically (say, via window.location = resp.redirect).

In addition to the redirect property, the response object will include a message key with the same text that would have been flashed (for a text/html response)—either a specific message from Craft, or one provided in the request via the globally-supported successMessage param. Additional action-specific properties are also returned at the top level of the response object.

# Failure

Failed responses are mostly handled via the craft\web\Controller::asModelFailure() (opens new window) or asFailure() (opens new window) methods.

# During a GET Request

A GET request will typically only fail if an exception is thrown in the process of generating a response. The criteria for that failure depends on the action, but can also be circumstantial—like a lost database connection.

If the request included an Accept: application/json header, Craft will send a message key in the JSON response, or a complete stack trace when devMode is on. Otherwise, Craft displays a standard error view.

# During a POST Request

POST requests can fail for the same reasons a GET request might—but because they are often responsible for mutating data, you’ll also be contending with validation errors.

We’ll use the term “model” here for technical reasons—but elements are models, too!

In all but rare, unrecoverable cases, Craft sets an error flash describing the issue, and carries on serving the page at the original path (either the page the request came from, or whatever was in the originating <form action="..."> attribute). By virtue of being part of the same request that populated and validated a model, Craft is able to pass it all the way through to the rendered template—making it possible to repopulate inputs and display errors, contextually. See a complete example of how to handle this in the models and validation section.

For requests that include an Accept: application/json header, Craft will instead build a JSON object with an errors key set to a list of the model’s errors (indexed by attribute or field), a message key, an array representation of the model, and a modelName key with the location of the model data in the payload. The exact message will be specific to the failure mode, and can be overridden using the globally-supported failMessage param.


  "errors": {
    "email": "...",
    /* ... */
  "modelName": "user",
  "user": {
    "email": "@craftcms",
    /* ... */

The value of modelName in a JSON response is the same as the variable name Craft uses for the model when rendering a template response. We’ll call this out in each action, below!

# Custom Fields

Actions that create or update elements (like entries/save-entry and users/save-address) support setting custom field values. Only fields that are included in a request will be updated.

Fields should be submitted under a fields key, using their handle:


<form method="post">
  {{ csrfInput() }}
  {{ actionInput('entries/save-entry') }}

  {{ input('text', 'fields[myCustomFieldHandle]') }}

  <button>Save Entry</button>

In the event you need to re-key the custom field data in the request, you can send a fieldsLocation param:



<form method="post">
  {{ csrfInput() }}
  {{ actionInput('entries/save-entry') }}
  {{ hiddenInput('fieldsLocation', 'f')}}

  {# Don’t forget to update all your input names! #}
  {{ input('text', 'f[myCustomFieldHandle]') }}

  <button>Save Entry</button>

# Field + Data Types

Fields (and attributes) that use scalar (opens new window) values like numbers, text, or booleans will work as expected with a single input.

Other types may require multiple inputs or specific naming conventions.

# Date + Time

Entries’ native postDate and expiryDate properties can be handled in the same way date/time fields are; but instead of passing their values under a fields key, you’ll send them as top-level keys in a POST request:

{{ input('datetime-local', 'postDate', entry.postDate|atom) }}

Both of these options will POST valid data that Craft can reconstruct into a PHP DateTime object.

Some date properties (like dateUpdated and dateCreated) may be determined by Craft, and are not editable.

# Relations

Assets, categories, entries, and tags can be associated to a relational field by passing an array of IDs. For more information and examples, see the relevant field type documentation: