10 min read

How to Upload an Image Using the WordPress REST API

Af Jonas Lindemann

Opdateret

A cURL command that uploads tankless-water-heater.jpg to the WordPress media endpoint with a 201 Created response, next to the blog post that shows the photo as its featured image

This tutorial uploads an image to the WordPress Media Library through the REST API, adds its alt text, title and caption, and makes it a post's featured image. It covers cURL, PHP, Node.js and Python, and the error messages you're most likely to see on the way.

Every request, response and screenshot comes from a WordPress 7.1.3 test site on PHP 8.3 and Apache. The example site belongs to a plumbing company that adds project photos to its blog.

The media endpoint

Uploads go to the media endpoint, /wp-json/wp/v2/media, as a POST request. WordPress takes the file in two forms: as the raw request body with a Content-Disposition header that names the file, or as a form upload with the file in a field called file. Both create the same attachment. The full list of fields is in the media endpoint reference.

The account you send needs permission to upload files. Authors, Editors and Administrators have it. In our test, Contributors and Subscribers got a 403 error.

Step 1: Create an application password

The REST API doesn't accept your normal login password. In our test, a request with the administrator's real password got the same 401 error as a request with no credentials at all. You need an application password, a separate password that WordPress creates for scripts and apps.

  1. In wp-admin, go to Users > Profile. To upload as another user, open that user under Users > All Users instead.
  2. Scroll down to Application Passwords.
  3. Enter a name under New Application Password Name, for example "Photo uploader", then click Add Application Password.
The Application Passwords section of the WordPress profile screen with Photo uploader entered as the name and the Add Application Password button highlighted

WordPress shows the new password once. Click Copy and keep it somewhere safe, because you can't look it up later.

The message Your new password for Photo uploader is, followed by the generated application password in a field, which is highlighted, and a Copy button

You can use the password with or without its spaces, and your username or your account's email address with it. Both worked in our test. To lock a script out later, click Revoke next to its password in the list below the form.

If the profile says "The application password feature requires HTTPS, which is not enabled on this site." instead of showing the form, see "Application passwords are missing from the profile" near the end of this tutorial.

Step 2: Check that WordPress accepts the password

The cURL examples in this tutorial use three shell variables. Set them first, with your site's address, your username and the application password:

bash
SITE="https://example.com"
WP_USER="your-username"
APP_PASSWORD="abcd EFGH 1234 ijkl MNOP 5678"

Then ask WordPress who you are:

bash
curl "$SITE/wp-json/wp/v2/users/me" --user "$WP_USER:$APP_PASSWORD"

A working password returns your user as JSON, starting with "id" and "name". If the answer contains rest_not_logged_in, WordPress didn't accept the credentials, and an upload with them fails too.

Step 3: Upload the image with cURL

Run this in the folder that contains the image:

bash
curl -X POST "$SITE/wp-json/wp/v2/media" \
  --user "$WP_USER:$APP_PASSWORD" \
  -H 'Content-Disposition: attachment; filename="tankless-water-heater.jpg"' \
  -H "Content-Type: image/jpeg" \
  --data-binary @tankless-water-heater.jpg

--data-binary sends the file as it is. The Content-Disposition header gives WordPress the file name, and it's required: without it, the request fails with "No Content-Disposition supplied." WordPress decides the file type itself. In our test, a JPEG sent with the wrong Content-Type was still stored as image/jpeg.

WordPress answers with status 201 and the new attachment. This is part of the response from our test:

json
1{
2  "id": 13,
3  "date": "2026-10-12T12:56:33",
4  "slug": "tankless-water-heater",
5  "type": "attachment",
6  "link": "https://example.com/tankless-water-heater/",
7  "title": {
8    "raw": "tankless-water-heater",
9    "rendered": "tankless-water-heater"
10  },
11  "author": 1,
12  "alt_text": "",
13  "media_type": "image",
14  "mime_type": "image/jpeg",
15  "post": null,
16  "source_url": "https://example.com/wp-content/uploads/2026/10/tankless-water-heater.jpg",
17  "media_details": {
18    "width": 1365,
19    "height": 1024,
20    "file": "2026/10/tankless-water-heater.jpg",
21    "filesize": 98292
22  }
23}

Note the id, because the next steps need it. source_url is the address of the file. WordPress also created the smaller image sizes, which are listed under media_details in the full response. The title is the file name without its extension, the alt text is empty, and post is null because the image isn't attached to anything yet. If a file with the same name exists already, WordPress adds -1 to the new file's name.

Step 4: Add alt text, a title and a caption

Send the fields to the image's own address. Replace 42 with the id from your upload:

bash
1curl -X POST "$SITE/wp-json/wp/v2/media/42" \
2  --user "$WP_USER:$APP_PASSWORD" \
3  -H "Content-Type: application/json" \
4  -d '{
5    "alt_text": "Tankless water heater mounted on a utility room wall",
6    "title": "Tankless water heater install",
7    "caption": "New tankless unit, installed in one day."
8  }'

WordPress answers with the updated attachment, and the new values show in the Media Library straight away.

Upload and add the details in one request

A form upload can carry the fields along with the file, so you don't need a second request. post attaches the image to a post; use the post's ID:

bash
1curl -X POST "$SITE/wp-json/wp/v2/media" \
2  --user "$WP_USER:$APP_PASSWORD" \
3  -F "file=@tankless-water-heater.jpg" \
4  -F "alt_text=Tankless water heater mounted on a utility room wall" \
5  -F "title=Tankless water heater install" \
6  -F "caption=New tankless unit, installed in one day." \
7  -F "post=7"

With -F, curl sends a form upload and takes the file name from the file itself. In our test, the alt text, title, caption and post all arrived with the file. In the Media Library, the image shows the three fields, and Uploaded to names the post:

Attachment details in the WordPress Media Library with Uploaded to showing the post Tankless water heater installed in Gresham, and the alternative text, title and caption set through the REST API highlighted

Step 5: Make it the post's featured image

The featured image is a setting of the post, so this request goes to the posts endpoint. Set featured_media to the image's ID and replace 7 with the post's ID:

bash
curl -X POST "$SITE/wp-json/wp/v2/posts/7" \
  --user "$WP_USER:$APP_PASSWORD" \
  -H "Content-Type: application/json" \
  -d '{"featured_media": 42}'

For a page, send the same body to /wp-json/wp/v2/pages/<page ID>. When you create a post through the REST API, you can also send featured_media in the same request as the title and content. Both worked in our test. For the post itself, see how to create a WordPress post programmatically.

Visitors now see the photo at the top of the post, with the alt text from Step 4:

The blog post Tankless water heater installed in Gresham on the Twenty Twenty-Five theme, with the uploaded photo of a wall-mounted tankless water heater as its featured image

If your theme doesn't show featured images at all, see how to add a featured image in WordPress without a plugin.

Upload an image from a URL (WordPress 7.1 and later)

WordPress 7.1 added a url field to the media endpoint. WordPress downloads the image from that address itself, so your script doesn't need the file. Send the address as JSON:

bash
curl -X POST "$SITE/wp-json/wp/v2/media" \
  --user "$WP_USER:$APP_PASSWORD" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://cdn.example.com/photos/tankless-water-heater.jpg"}'

The address has to end in an image file name, such as .jpg or .png. Other addresses fail with "The provided URL does not point to a supported image file." before anything is downloaded.

In our test, WordPress ignored alt_text, title and caption in this request and used the file name as the title, while post worked. Set the details with a second request, as in Step 4. On WordPress 7.0 and older, the url field doesn't exist, so download the image in your script and upload the file as in Step 3.

Upload with PHP

This script uses PHP's cURL extension, so it runs on any server with PHP, inside WordPress or not. We ran it with PHP 8.3:

upload.phpphp
1<?php
2$site         = 'https://example.com';
3$user         = 'your-username';
4$app_password = 'abcd EFGH 1234 ijkl MNOP 5678';
5$file         = __DIR__ . '/tankless-water-heater.jpg';
6
7$ch = curl_init( $site . '/wp-json/wp/v2/media' );
8curl_setopt_array( $ch, array(
9    CURLOPT_POST           => true,
10    CURLOPT_USERPWD        => $user . ':' . $app_password,
11    CURLOPT_HTTPHEADER     => array(
12        'Content-Disposition: attachment; filename="' . basename( $file ) . '"',
13        'Content-Type: ' . mime_content_type( $file ),
14    ),
15    CURLOPT_POSTFIELDS     => file_get_contents( $file ),
16    CURLOPT_RETURNTRANSFER => true,
17) );
18$body   = curl_exec( $ch );
19$status = curl_getinfo( $ch, CURLINFO_RESPONSE_CODE );
20
21$media = json_decode( $body, true );
22if ( 201 === $status ) {
23    echo 'Uploaded as media ID ' . $media['id'] . ': ' . $media['source_url'] . PHP_EOL;
24} else {
25    echo 'Upload failed (' . $status . '): ' . $media['message'] . PHP_EOL;
26}

It prints the new media ID and file address, or the status code and WordPress's error message.

Upload with JavaScript (Node.js)

This version uses the fetch function built into Node.js. Save it as upload.mjs and run node upload.mjs. We ran it with Node.js 22:

upload.mjsjavascript
1import { readFile } from 'node:fs/promises'
2import { basename } from 'node:path'
3
4const site = 'https://example.com'
5const user = 'your-username'
6const appPassword = 'abcd EFGH 1234 ijkl MNOP 5678'
7const file = 'tankless-water-heater.jpg'
8
9const response = await fetch(`${site}/wp-json/wp/v2/media`, {
10  method: 'POST',
11  headers: {
12    Authorization: 'Basic ' + Buffer.from(`${user}:${appPassword}`).toString('base64'),
13    'Content-Disposition': `attachment; filename="${basename(file)}"`,
14    'Content-Type': 'image/jpeg',
15  },
16  body: await readFile(file),
17})
18const media = await response.json()
19
20if (response.status === 201) console.log(`Uploaded as media ID ${media.id}: ${media.source_url}`)
21else console.log(`Upload failed (${response.status}): ${media.message}`)

Keep code like this on a server or your own computer. In a browser on a public page, anyone could read the application password from the page's source.

Upload with Python

This version needs the requests library (pip install requests). We ran it with Python 3.14 and requests 2.34:

upload.pypython
1import os
2import requests
3
4site = "https://example.com"
5user = "your-username"
6app_password = "abcd EFGH 1234 ijkl MNOP 5678"
7path = "tankless-water-heater.jpg"
8
9with open(path, "rb") as image:
10    response = requests.post(
11        f"{site}/wp-json/wp/v2/media",
12        auth=(user, app_password),
13        headers={
14            "Content-Disposition": f'attachment; filename="{os.path.basename(path)}"',
15            "Content-Type": "image/jpeg",
16        },
17        data=image,
18    )
19
20media = response.json()
21if response.status_code == 201:
22    print(f"Uploaded as media ID {media['id']}: {media['source_url']}")
23else:
24    print(f"Upload failed ({response.status_code}): {media['message']}")

"Sorry, you are not allowed to create posts as this user." (401)

WordPress didn't accept the login. The message mentions posts even when you upload media. In our tests, it came back for the account's normal password, a mistyped application password, an application password that belongs to a different user, and any application password on a site where they're switched off. Run the request from Step 2 to test the credentials on their own.

The same message with status 403

WordPress knows who you are, but the account isn't allowed to upload files. Contributors and Subscribers can't upload, through the API or in wp-admin. Create the application password for an Author, Editor or Administrator instead.

Application passwords are missing from the profile

WordPress only offers application passwords on sites that use HTTPS. Without it, the profile shows this message instead of the form, and existing application passwords stop working:

The Application Passwords section of a WordPress profile with the highlighted message The application password feature requires HTTPS, which is not enabled on this site, and a link to set the environment type

On a live site, switch to HTTPS. On a local development site without HTTPS, add this line to wp-config.php, above the line /* That's all, stop editing! Happy publishing. */:

wp-config.phpphp
define( 'WP_ENVIRONMENT_TYPE', 'local' );

In our test, the form came back and the same application password worked after this change.

"No Content-Disposition supplied."

WordPress didn't get a file name. Add the Content-Disposition header to a raw upload, as in Step 3. In our test, a form upload larger than PHP's post_max_size setting (8 MB on that server) caused the same message, because PHP throws away the whole request body. Send large files as a raw upload with --data-binary, or ask your host to raise the limit.

"The uploaded file exceeds the upload_max_filesize directive in php.ini."

Form uploads are limited by PHP's upload_max_filesize. On our test server it was 2 MB, and a 3 MB form upload failed with this message. The same file sent as a raw upload went through, and so did a 9 MB file. Send large files raw, or ask your host to raise the limit.

"Sorry, you are not allowed to upload this file type."

The file name in Content-Disposition needs an extension that WordPress allows, such as .jpg or .png. In our test, a file name without an extension and an SVG file both failed with this message and status 500. WordPress doesn't accept SVG uploads unless a plugin or your own code allows them.

To get images back out of WordPress, see how to download images from the WordPress Media Library.

About the Author

I've worked in SEO for more than ten years and helped over 100 businesses rank higher, mostly local businesses, e-commerce stores and SaaS companies. As co-founder of LPagery I work on local SEO, programmatic SEO and AI search visibility, and I test the strategies in these articles on our own sites and with our customers.