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.