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

---

# WP_REST_Attachments_Controller::finalize_item( 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/finalize_item/?output_format=md#description)
 * [Parameters](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/finalize_item/?output_format=md#parameters)
 * [Return](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/finalize_item/?output_format=md#return)
 * [Source](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/finalize_item/?output_format=md#source)
 * [Hooks](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/finalize_item/?output_format=md#hooks)
 * [Related](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/finalize_item/?output_format=md#related)
 * [Changelog](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/finalize_item/?output_format=md#changelog)

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

Finalizes an attachment after client-side media processing.

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

Applies the sub-size metadata collected from sideload responses in a single metadata
update, then triggers the ‘wp_generate_attachment_metadata’ filter so that server-
side plugins can process the attachment after all client-side operations (upload,
thumbnail generation, sideloads) are complete.

## 󠀁[Parameters](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/finalize_item/?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/finalize_item/?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/finalize_item/?output_format=md#source)󠁿

    ```php
    public function finalize_item( WP_REST_Request $request ) {
    	$attachment_id = (int) $request['id'];

    	$post = $this->get_post( $attachment_id );
    	if ( is_wp_error( $post ) ) {
    		return $post;
    	}

    	/**
    	 * Sub-size metadata collected from sideload responses. Confirm every
    	 * file name was produced by a prior sideload for this attachment before
    	 * storing it, so a client cannot make finalize record (and later read or
    	 * delete) another attachment's files.
    	 *
    	 * @var list<Image_Sub_Size> $sub_sizes
    	 */
    	$sub_sizes  = $request['sub_sizes'] ?? array();
    	$provenance = $this->validate_sub_size_provenance( $attachment_id, $sub_sizes );
    	if ( is_wp_error( $provenance ) ) {
    		return $provenance;
    	}

    	$metadata = wp_get_attachment_metadata( $attachment_id );
    	if ( ! is_array( $metadata ) ) {
    		$metadata = array();
    	}

    	// Apply all sub-size metadata collected from sideload responses.
    	foreach ( $sub_sizes as $sub_size ) {
    		$image_size = $sub_size['image_size'];

    		// When multiple size names share identical dimensions the client
    		// sends a single sub-size entry with an array of names. Register the
    		// same file under each name.
    		if ( is_array( $image_size ) ) {
    			/*
    			 * Arrays carry regular sizes only, as the sideload endpoint
    			 * enforces. Each special size names a single file handled by one
    			 * of the branches below, so grouping one under a shared file
    			 * would write it to the wrong place; reject rather than guess.
    			 */
    			if ( array_intersect( $image_size, self::get_special_image_sizes() ) ) {
    				return new WP_Error(
    					'rest_invalid_sub_size_name',
    					__( 'A grouped sub-size entry may only name regular image sizes.' ),
    					array( 'status' => 400 )
    				);
    			}

    			// As below: `file` is not required by the schema, and a size
    			// entry that names no file is not worth recording.
    			if ( empty( $sub_size['file'] ) ) {
    				continue;
    			}

    			$metadata['sizes'] = $metadata['sizes'] ?? array();

    			foreach ( $image_size as $name ) {
    				$metadata['sizes'][ $name ] = array(
    					'width'     => $sub_size['width'] ?? 0,
    					'height'    => $sub_size['height'] ?? 0,
    					'file'      => $sub_size['file'],
    					'mime-type' => $sub_size['mime_type'] ?? '',
    					'filesize'  => $sub_size['filesize'] ?? 0,
    				);
    			}
    			continue;
    		}

    		if ( 'original' === $image_size || 'scaled' === $image_size ) {
    			// Skip malformed entries so a bad payload cannot blank out the
    			// main file metadata.
    			if ( empty( $sub_size['file'] ) ) {
    				continue;
    			}

    			/*
    			 * Record the supplied full-size image (from sideload_item()) as
    			 * the main file, keeping the current attached file as
    			 * `original_image`. A 'scaled' image is downsized and an
    			 * 'original' image is rotated; both have any EXIF orientation
    			 * already applied by the client.
    			 */
    			if ( ! empty( $sub_size['original_image'] ) ) {
    				$metadata['original_image'] = $sub_size['original_image'];
    			}
    			$metadata['width']    = $sub_size['width'] ?? 0;
    			$metadata['height']   = $sub_size['height'] ?? 0;
    			$metadata['filesize'] = $sub_size['filesize'] ?? 0;
    			$metadata['file']     = $sub_size['file'];

    			/*
    			 * The supplied image has its orientation applied already, so
    			 * reset the stored value (from the upload) to 1, as
    			 * wp_create_image_subsizes() does for both its scale and rotate
    			 * paths. Otherwise exif_orientation would still report the
    			 * pre-rotation value and the client would rotate the image
    			 * again on a re-fetch.
    			 */
    			if ( ! empty( $metadata['image_meta']['orientation'] ) ) {
    				$metadata['image_meta']['orientation'] = 1;
    			}
    		} elseif ( self::IMAGE_SIZE_SOURCE_ORIGINAL === $image_size ) {
    			// As above: `file` is not required by the schema, and each of
    			// these sizes is nothing but the file it names.
    			if ( empty( $sub_size['file'] ) ) {
    				continue;
    			}

    			/*
    			 * Source-format original: stored under its own meta key so the
    			 * scaled-sideload flow (which writes 'original_image') cannot
    			 * clobber it. 'original_image' keeps pointing at the
    			 * web-viewable JPEG derivative. Cleanup on attachment delete
    			 * is handled by wp_delete_attachment_files().
    			 */
    			$metadata[ self::META_KEY_SOURCE_IMAGE ] = $sub_size['file'];
    		} elseif ( 'animated_video' === $image_size ) {
    			if ( empty( $sub_size['file'] ) ) {
    				continue;
    			}

    			/*
    			 * Converted-video companion of an animated GIF. Stored under its
    			 * own meta key; 'original_image' keeps pointing at the GIF. Cleanup
    			 * on attachment delete is handled by wp_delete_attachment_files().
    			 */
    			$metadata['animated_video'] = $sub_size['file'];
    		} elseif ( 'animated_video_poster' === $image_size ) {
    			if ( empty( $sub_size['file'] ) ) {
    				continue;
    			}

    			// Static first-frame poster for the converted video.
    			$metadata['animated_video_poster'] = $sub_size['file'];
    		} else {
    			if ( empty( $sub_size['file'] ) ) {
    				continue;
    			}

    			$metadata['sizes'] = $metadata['sizes'] ?? array();

    			$metadata['sizes'][ $image_size ] = array(
    				'width'     => $sub_size['width'] ?? 0,
    				'height'    => $sub_size['height'] ?? 0,
    				'file'      => $sub_size['file'],
    				'mime-type' => $sub_size['mime_type'] ?? '',
    				'filesize'  => $sub_size['filesize'] ?? 0,
    			);
    		}
    	}

    	/** This filter is documented in wp-admin/includes/image.php */
    	$metadata = apply_filters( 'wp_generate_attachment_metadata', $metadata, $attachment_id, 'update' );

    	wp_update_attachment_metadata( $attachment_id, $metadata );

    	/*
    	 * Drop only the provenance rows this request consumed, now that the
    	 * names are recorded in the metadata itself. A row is dropped only once
    	 * its name is recoverable from the stored metadata, so a name the
    	 * 'wp_generate_attachment_metadata' filter removed - or that a failed
    	 * update never persisted - keeps its row and the retried request the
    	 * endpoint documents as idempotent still validates. Rows for sideloads
    	 * that have not been finalized yet survive for a later call, and passing
    	 * the value makes the delete a no-op when the row is already gone, so a
    	 * retried request cleans up without error. Any rows left behind by an
    	 * abandoned upload are removed with the attachment itself.
    	 *
    	 * Retrying is idempotent for the request as it was sent. A name is only
    	 * unavailable to a retry once a later finalize has overwritten the same
    	 * size with a newly sideloaded file, which drops the earlier name from
    	 * the metadata the retry recovers it from.
    	 *
    	 * The names are collected before deleting so a request which repeats
    	 * the same name across many sub-sizes still issues one query per
    	 * distinct name.
    	 */
    	$recoverable = $this->get_sideloaded_file_names( $attachment_id, false );
    	$consumed    = array();
    	foreach ( $sub_sizes as $sub_size ) {
    		foreach ( array( 'file', 'original_image' ) as $key ) {
    			// Matches the set validate_sub_size_provenance() checked, so
    			// every name a request was allowed to store is also cleaned up.
    			if (
    				isset( $sub_size[ $key ] ) &&
    				is_string( $sub_size[ $key ] ) &&
    				in_array( $sub_size[ $key ], $recoverable, true )
    			) {
    				$consumed[] = $sub_size[ $key ];
    			}
    		}
    	}

    	foreach ( array_unique( $consumed ) as $file_name ) {
    		delete_post_meta( $attachment_id, self::META_KEY_SIDELOAD_FILE_NAME, wp_slash( $file_name ) );
    	}

    	$response_request = new WP_REST_Request(
    		WP_REST_Server::READABLE,
    		rest_get_route_for_post( $attachment_id )
    	);

    	$response_request['context'] = 'edit';

    	if ( isset( $request['_fields'] ) ) {
    		$response_request['_fields'] = $request['_fields'];
    	}

    	return $this->prepare_item_for_response( $post, $response_request );
    }
    ```

[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#L3247)
[View on GitHub](https://github.com/WordPress/wordpress-develop/blob/7.1/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php#L3247-L3457)

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

 [apply_filters( ‘wp_generate_attachment_metadata’, array $metadata, int $attachment_id, string $context )](https://developer.wordpress.org/reference/hooks/wp_generate_attachment_metadata/)

Filters the generated attachment meta data.

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

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

Validates the `sub_sizes` file names against what this attachment produced.

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

Returns the image size names which name a single file rather than a sub-size.

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

Returns the file names which a finalize request may store for an attachment.

  | 
| [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::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_REST_Request::__construct()](https://developer.wordpress.org/reference/classes/wp_rest_request/__construct/)`wp-includes/rest-api/class-wp-rest-request.php` |

Constructor.

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

Retrieves attachment metadata for attachment ID.

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

Updates metadata for an attachment.

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

Deletes a post meta field for the given post ID.

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

Retrieves the translation of $text.

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

Adds slashes to a string or recursively adds slashes to strings within an array.

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

Calls the callback functions that have been added to a filter hook.

  | 
| [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 9 more](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/finalize_item/?output_format=md#)
[Show less](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/finalize_item/?output_format=md#)

## 󠀁[Changelog](https://developer.wordpress.org/reference/classes/wp_rest_attachments_controller/finalize_item/?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%2Ffinalize_item%2F)
before being able to contribute a note or feedback.