Programmatic File Uploads in WordPress with wp_handle_upload and wp_insert_attachment

WordPress provides a complete API for handling file uploads programmatically. Whether you are building a custom front-end upload form or processing files inside a plugin, the combination of wp_handle_upload() and wp_insert_attachment() gives you full control over where files are stored and how they appear in the Media Library.

Problem: How do you accept a file upload from a custom front-end form, save it to the WordPress media library, and optionally associate it with a post — without using the standard admin uploader?

Solution: Validate and move the uploaded file with wp_handle_upload(), generate attachment metadata with wp_generate_attachment_metadata(), insert the record into the media library with wp_insert_attachment(), and link it to a post with set_post_thumbnail() or update_post_meta().

Step 1. Create the HTML form. Make sure to set enctype="multipart/form-data" and include a nonce for security:

<form action="" method="post" enctype="multipart/form-data">
    <input type="file" name="my_upload" accept="image/*" />
    <input type="hidden" name="my_upload_nonce" value="<?php echo wp_create_nonce( 'my_upload_nonce' ); ?>" />
    <button type="submit" name="my_upload_submit">Upload</button>
</form>

Step 2. Handle the upload on the server side. Validate the nonce, then pass the file to wp_handle_upload(). This function moves the file to the uploads directory and returns its URL and path:

<?php
if (
    isset( $_POST['my_upload_submit'] ) &&
    isset( $_FILES['my_upload'] ) &&
    wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['my_upload_nonce'] ) ), 'my_upload_nonce' )
) {
    require_once ABSPATH . 'wp-admin/includes/file.php';

    $upload_overrides = [ 'test_form' => false ];
    $uploaded_file    = wp_handle_upload( $_FILES['my_upload'], $upload_overrides );

    if ( isset( $uploaded_file['error'] ) ) {
        // Handle error
        wp_die( esc_html( $uploaded_file['error'] ) );
    }

    // $uploaded_file now contains 'file' (absolute path) and 'url'
}

Step 3. Insert the uploaded file as an attachment so it appears in the Media Library and its metadata is indexed:

<?php
// Continued from Step 2 …
require_once ABSPATH . 'wp-admin/includes/image.php';

$file_path = $uploaded_file['file'];
$file_url  = $uploaded_file['url'];
$file_type = wp_check_filetype( basename( $file_path ), null );

$attachment = [
    'post_mime_type' => $file_type['type'],
    'post_title'     => sanitize_file_name( basename( $file_path ) ),
    'post_content'   => '',
    'post_status'    => 'inherit',
];

// Attach to a specific post (0 = unattached / in the library)
$parent_post_id = 0;
$attach_id      = wp_insert_attachment( $attachment, $file_path, $parent_post_id );

// Generate and save attachment metadata (thumbnails, dimensions, etc.)
$attach_data = wp_generate_attachment_metadata( $attach_id, $file_path );
wp_update_attachment_metadata( $attach_id, $attach_data );

// Optional: set as featured image of a post
// set_post_thumbnail( $parent_post_id, $attach_id );

After this code runs, the file will be visible in Media → Library, complete with generated thumbnail sizes and EXIF data (for images). The attachment ID can be stored in post meta or used immediately with functions like wp_get_attachment_image().

NOTE: wp_handle_upload() performs MIME type validation internally, but you should also restrict accepted file types on the front end with the accept attribute and verify the MIME type server-side using wp_check_filetype() before calling wp_insert_attachment(). Never trust the file extension alone.