Title: WP_REST_Attachments_Controller::create_item_from_url
Published: August 20, 2026

---

# WP_REST_Attachments_Controller::create_item_from_url( WP_REST_Request $request ): 󠀁[WP_REST_Response](https://developer.wordpress.org/reference/classes/wp_rest_response/)󠁿|󠀁[WP_Error](https://developer.wordpress.org/reference/classes/wp_error/)󠁿

## In this article

 * [Description](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/create_item_from_url/?output_format=md#description)
 * [Parameters](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/create_item_from_url/?output_format=md#parameters)
 * [Return](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/create_item_from_url/?output_format=md#return)
 * [Source](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/create_item_from_url/?output_format=md#source)
 * [Hooks](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/create_item_from_url/?output_format=md#hooks)
 * [Related](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/create_item_from_url/?output_format=md#related)
 * [Changelog](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/create_item_from_url/?output_format=md#changelog)

[ Back to top](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/create_item_from_url/?output_format=md#wp--skip-link--target)

Sideloads an external image from a URL into the media library.

## 󠀁[Description](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/create_item_from_url/?output_format=md#description)󠁿

Downloads the remote file on the server, avoiding a cross-origin browser fetch that
fails under cross-origin isolation. Whether sub-sizes are generated is governed 
by the filters applied in create_item().

## 󠀁[Parameters](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/create_item_from_url/?output_format=md#parameters)󠁿

 `$request`[WP_REST_Request](https://developer.wordpress.org/reference/classes/wp_rest_request/)
required

Full details about the request.

## 󠀁[Return](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/create_item_from_url/?output_format=md#return)󠁿

 [WP_REST_Response](https://developer.wordpress.org/reference/classes/wp_rest_response/)
|[WP_Error](https://developer.wordpress.org/reference/classes/wp_error/) Response
object on success, [WP_Error](https://developer.wordpress.org/reference/classes/wp_error/)
object on failure.

## 󠀁[Source](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/create_item_from_url/?output_format=md#source)󠁿

    ```php
    protected function create_item_from_url( WP_REST_Request $request ) {
    	// Sideloading downloads and stores a file, so require the upload capability.
    	if ( ! current_user_can( 'upload_files' ) ) {
    		return new WP_Error(
    			'rest_cannot_create',
    			__( 'Sorry, you are not allowed to upload media on this site.' ),
    			array( 'status' => rest_authorization_required_code() )
    		);
    	}

    	require_once ABSPATH . 'wp-admin/includes/file.php';
    	require_once ABSPATH . 'wp-admin/includes/media.php';
    	require_once ABSPATH . 'wp-admin/includes/image.php';

    	$url     = $request['url'];
    	$post_id = ! empty( $request['post'] ) ? (int) $request['post'] : 0;

    	// Derive the filename from the URL path before downloading anything.
    	$url_path = wp_parse_url( $url, PHP_URL_PATH );
    	$filename = $url_path ? wp_basename( $url_path ) : '';
    	if ( '' === $filename ) {
    		return new WP_Error(
    			'rest_invalid_url',
    			__( 'Could not determine a filename from the provided URL.' ),
    			array( 'status' => 400 )
    		);
    	}

    	/*
    	 * Only download URLs whose extension maps to an allowed image MIME type.
    	 * The sideload handler would reject other types anyway (via
    	 * wp_check_filetype_and_ext()), but checking first avoids downloading
    	 * files that can never be accepted, such as PHP scripts.
    	 */
    	$filetype = wp_check_filetype( $filename );
    	if ( ! $filetype['type'] || ! str_starts_with( $filetype['type'], 'image/' ) ) {
    		return new WP_Error(
    			'rest_invalid_url',
    			__( 'The provided URL does not point to a supported image file.' ),
    			array( 'status' => 400 )
    		);
    	}

    	/*
    	 * Cap the download at the same size the site would accept as a direct
    	 * upload. check_upload_size() only applies on multisite, so without a
    	 * ceiling here a single site has no limit at all on this path: the
    	 * `upload_max_filesize` and `post_max_size` directives bound a request
    	 * body, not a fetch the server makes itself.
    	 *
    	 * When `wp_max_upload_size` returns 0, no ceiling is applied.
    	 */
    	$max_size = (int) wp_max_upload_size();

    	/*
    	 * Download the remote file with WordPress's HTTP API, which validates
    	 * the host and blocks requests to private or local addresses. This is
    	 * the same primitive core's media_sideload_image() relies on.
    	 *
    	 * `limit_response_size` stops the transfer once the limit is passed,
    	 * so an oversized remote file is never written to disk in full. One
    	 * byte over the ceiling is enough to fail the size check below.
    	 */
    	$limit_response_size = static function ( $args ) use ( $max_size ) {
    		$args['limit_response_size'] = $max_size + 1;
    		return $args;
    	};

    	if ( $max_size > 0 ) {
    		add_filter( 'http_request_args', $limit_response_size );
    	}

    	$tmp_file = download_url( $url );

    	if ( $max_size > 0 ) {
    		remove_filter( 'http_request_args', $limit_response_size );
    	}

    	if ( is_wp_error( $tmp_file ) ) {
    		return $tmp_file;
    	}

    	$file_array = array(
    		'name'     => $filename,
    		'tmp_name' => $tmp_file,
    	);

    	$size_check = self::check_upload_size( $file_array );
    	if ( is_wp_error( $size_check ) ) {
    		if ( file_exists( $tmp_file ) ) {
    			wp_delete_file( $tmp_file );
    		}
    		return $size_check;
    	}

    	if ( $max_size > 0 && wp_filesize( $tmp_file ) > $max_size ) {
    		if ( file_exists( $tmp_file ) ) {
    			wp_delete_file( $tmp_file );
    		}

    		return new WP_Error(
    			'rest_upload_file_too_big',
    			/* translators: %s: Maximum allowed file size in kilobytes. */
    			sprintf( __( 'This file is too big. Files must be less than %s KB in size.' ), number_format( $max_size / KB_IN_BYTES ) ),
    			array( 'status' => 400 )
    		);
    	}

    	$attachment_id = media_handle_sideload( $file_array, $post_id );

    	if ( is_wp_error( $attachment_id ) ) {
    		/*
    		 * media_handle_sideload() deletes the temp file on success; remove
    		 * it explicitly when the sideload fails.
    		 */
    		if ( file_exists( $tmp_file ) ) {
    			wp_delete_file( $tmp_file );
    		}
    		return $attachment_id;
    	}

    	$attachment = get_post( $attachment_id );

    	$request->set_param( 'context', 'edit' );

    	/*
    	 * media_handle_sideload() fires the standard insert hooks (including
    	 * wp_after_insert_post), but not the REST-specific action, so fire it
    	 * here for parity with the uploaded-file path in create_item().
    	 */
    	/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php */
    	do_action( 'rest_after_insert_attachment', $attachment, $request, true );

    	$response = $this->prepare_item_for_response( $attachment, $request );
    	$response->set_status( 201 );
    	$response->header( 'Location', rest_url( rest_get_route_for_post( $attachment_id ) ) );

    	return $response;
    }
    ```

[View all references](https://developer.wordpress.org/reference/files/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php/)
[View on Trac](https://core.trac.wordpress.org/browser/tags/7.1/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php#L654)
[View on GitHub](https://github.com/WordPress/wordpress-develop/blob/7.1/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php#L654-L792)

## 󠀁[Hooks](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/create_item_from_url/?output_format=md#hooks)󠁿

 [do_action( ‘rest_after_insert_attachment’, WP_Post $attachment, WP_REST_Request $request, bool $creating )](https://developer.wordpress.org/reference/hooks/rest_after_insert_attachment/)

Fires after a single attachment is completely created or updated via the REST API.

## 󠀁[Related](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/create_item_from_url/?output_format=md#related)󠁿

| Uses | Description | 
| [wp_filesize()](https://developer.wordpress.org/reference/functions/wp_filesize/)`wp-includes/functions.php` |

Wrapper for PHP filesize with filters and casting the result as an integer.

  | 
| [rest_get_route_for_post()](https://developer.wordpress.org/reference/functions/rest_get_route_for_post/)`wp-includes/rest-api.php` |

Gets the REST API route for a post.

  | 
| [WP_REST_Attachments_Controller::check_upload_size()](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/check_upload_size/)`wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php` |

Determine if uploaded file exceeds space quota on multisite.

  | 
| [WP_REST_Attachments_Controller::prepare_item_for_response()](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/prepare_item_for_response/)`wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php` |

Prepares a single attachment output for response.

  | 
| [wp_parse_url()](https://developer.wordpress.org/reference/functions/wp_parse_url/)`wp-includes/http.php` |

A wrapper for PHP’s parse_url() function that handles consistency in the return values across PHP versions.

  | 
| [wp_delete_file()](https://developer.wordpress.org/reference/functions/wp_delete_file/)`wp-includes/functions.php` |

Deletes a file.

  | 
| [media_handle_sideload()](https://developer.wordpress.org/reference/functions/media_handle_sideload/)`wp-admin/includes/media.php` |

Handles a side-loaded file in the same way as an uploaded file is handled by [media_handle_upload()](https://developer.wordpress.org/reference/functions/media_handle_upload/) .

  | 
| [download_url()](https://developer.wordpress.org/reference/functions/download_url/)`wp-admin/includes/file.php` |

Downloads a URL to a local temporary file using the WordPress HTTP API.

  | 
| [wp_check_filetype()](https://developer.wordpress.org/reference/functions/wp_check_filetype/)`wp-includes/functions.php` |

Retrieves the file type from the file name.

  | 
| [wp_max_upload_size()](https://developer.wordpress.org/reference/functions/wp_max_upload_size/)`wp-includes/media.php` |

Determines the maximum upload size allowed in php.ini.

  | 
| [rest_authorization_required_code()](https://developer.wordpress.org/reference/functions/rest_authorization_required_code/)`wp-includes/rest-api.php` |

Returns a contextual HTTP error code for authorization failure.

  | 
| [rest_url()](https://developer.wordpress.org/reference/functions/rest_url/)`wp-includes/rest-api.php` |

Retrieves the URL to a REST endpoint.

  | 
| [current_user_can()](https://developer.wordpress.org/reference/functions/current_user_can/)`wp-includes/capabilities.php` |

Returns whether the current user has the specified capability.

  | 
| [__()](https://developer.wordpress.org/reference/functions/__/)`wp-includes/l10n.php` |

Retrieves the translation of $text.

  | 
| [wp_basename()](https://developer.wordpress.org/reference/functions/wp_basename/)`wp-includes/formatting.php` |

i18n-friendly version of basename().

  | 
| [add_filter()](https://developer.wordpress.org/reference/functions/add_filter/)`wp-includes/plugin.php` |

Adds a callback function to a filter hook.

  | 
| [remove_filter()](https://developer.wordpress.org/reference/functions/remove_filter/)`wp-includes/plugin.php` |

Removes a callback function from a filter hook.

  | 
| [do_action()](https://developer.wordpress.org/reference/functions/do_action/)`wp-includes/plugin.php` |

Calls the callback functions that have been added to an action hook.

  | 
| [get_post()](https://developer.wordpress.org/reference/functions/get_post/)`wp-includes/post.php` |

Retrieves post data given a post ID or post object.

  | 
| [is_wp_error()](https://developer.wordpress.org/reference/functions/is_wp_error/)`wp-includes/load.php` |

Checks whether the given variable is a WordPress Error.

  | 
| [WP_Error::__construct()](https://developer.wordpress.org/reference/classes/wp_error/__construct/)`wp-includes/class-wp-error.php` |

Initializes the error.

  |

[Show 16 more](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/create_item_from_url/?output_format=md#)
[Show less](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/create_item_from_url/?output_format=md#)

| Used by | Description | 
| [WP_REST_Attachments_Controller::create_item()](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/create_item/)`wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php` |

Creates a single attachment.

  |

## 󠀁[Changelog](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/create_item_from_url/?output_format=md#changelog)󠁿

| Version | Description | 
| [7.1.0](https://developer.wordpress.org/reference/since/7.1.0/) | Introduced. |

## User Contributed Notes

You must [log in](https://login.wordpress.org/?redirect_to=https%3A%2F%2Fdeveloper.wordpress.org%2Freference%2Fclasses%2Fwp_rest_attachments_controller%2Fcreate_item_from_url%2F)
before being able to contribute a note or feedback.