WordPress admin_post Example: Handle Custom Form Submission Safely
Use admin-post.php for secure WordPress form handling with nonce checks, capability gates, sanitization, and safe redirects.
Published
April 24, 2026
Reading Time
2 min read
Updated
April 24, 2026

Implementation Notes
Extension points, code paths, and implementation choices that should survive contact with production.
Best For
WordPress developers, agencies, and technical teams building custom plugin or theme functionality with cleaner operational defaults.
Primary Topics
Editorial Focus
Build Pattern: Extension points, code paths, and implementation choices that should survive contact with production. Updated on April 24, 2026.
Full Report
Last reviewed: April 24, 2026
Not every WordPress form needs AJAX or a custom REST endpoint. For many back-office workflows, a direct request to wp-admin/admin-post.php is the simplest and safest route because WordPress already gives you a handler entry point, authenticated routing, and a predictable redirect pattern.
This guide shows how to build a custom admin form handler with admin_post_{$action}, a nonce, strict capability checks, sanitized input, and a safe redirect back to the UI.
Build the form
<form action=\"<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>\" method=\"post\">
<input type=\"hidden\" name=\"action\" value=\"vulnwp_save_notice\" />
<?php wp_nonce_field( 'vulnwp_save_notice_action', 'vulnwp_notice_nonce' ); ?>
<label for=\"vulnwp-notice-text\">Homepage notice</label>
<input id=\"vulnwp-notice-text\" type=\"text\" name=\"notice_text\" value=\"\" class=\"regular-text\" />
<?php submit_button( __( 'Save notice', 'vulnwp' ) ); ?>
</form>
The hidden action value is the router key. WordPress turns that value into the hook name that receives the request.
Register the handler
<?php
add_action( 'admin_post_vulnwp_save_notice', 'vulnwp_handle_notice_form' );
function vulnwp_handle_notice_form() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You are not allowed to perform this action.', 'vulnwp' ), 403 );
}
if ( ! isset( $_POST['vulnwp_notice_nonce'] ) ) {
wp_die( esc_html__( 'Missing security token.', 'vulnwp' ), 400 );
}
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['vulnwp_notice_nonce'] ) ), 'vulnwp_save_notice_action' ) ) {
wp_die( esc_html__( 'Invalid security token.', 'vulnwp' ), 403 );
}
$notice = isset( $_POST['notice_text'] )
? sanitize_text_field( wp_unslash( $_POST['notice_text'] ) )
: '';
update_option( 'vulnwp_home_notice', $notice, false );
$redirect = add_query_arg(
array(
'page' => 'vulnwp-settings',
'updated' => '1',
),
admin_url( 'options-general.php' )
);
wp_safe_redirect( $redirect );
exit;
}
The handler exits after the redirect. That matters because WordPress does not stop execution automatically after a redirect header is sent.
Public forms need the non-privileged hook too
If a form should accept requests from users who are not logged in, register both the authenticated and unauthenticated actions. Then apply your own validation model carefully, because there is no user session to rely on.
add_action( 'admin_post_nopriv_vulnwp_capture_signup', 'vulnwp_capture_signup' );
add_action( 'admin_post_vulnwp_capture_signup', 'vulnwp_capture_signup' );
Do not add the nopriv version unless it is truly needed. A public handler expands the attack surface immediately.
When to use admin-post instead of REST or AJAX
- Use
admin-post.phpfor simple form submissions and redirect-based admin flows. - Use REST when the caller expects structured JSON responses.
- Use admin AJAX when you need legacy asynchronous behavior inside wp-admin.
Production checklist
- Keep the action name prefixed. Hook collisions are avoidable.
- Check the exact capability needed. Do not over-grant access.
- Verify the nonce before reading request data deeply.
- Sanitize every field. Each input type needs an explicit rule.
- Redirect with
wp_safe_redirect()andexit.
Common mistakes
- Forgetting the hidden
actionfield. The handler will never fire. - No nonce. Authenticated forms still need CSRF protection.
- Using
manage_optionseverywhere. Many handlers need a narrower capability. - Returning raw output instead of redirecting the user. Admin flows become inconsistent fast.
- Adding a public handler accidentally.
admin_post_nopriv_*should be a deliberate choice.
Related reading
If the form changes site settings, pair this with the Settings API example. For redirect hygiene after the handler completes, read the wp_safe_redirect guide.


