# Uploading images to ghost Admin-API

**URL:** <https://forum.ghost.org/t/uploading-images-to-ghost-admin-api/41348>\
**Category:** Developer help\
**Created:** [September 20, 2023, 6:04am UTC](https://forum.ghost.org/t/uploading-images-to-ghost-admin-api/41348 "2023-09-20T06:04:09Z")\
**Posts on this page:** 16\
**Page:** 1

<div class="post-metadata">

**Author:** ![Hailst0rm](https://sea1.discourse-cdn.com/flex015/user_avatar/forum.ghost.org/hailst0rm/32/26198_2.png) [@Hailst0rm](https://forum.ghost.org/u/Hailst0rm)\
**Post date:** [September 20, 2023, 6:04am UTC](https://forum.ghost.org/t/uploading-images-to-ghost-admin-api/41348/1 "2023-09-20T06:04:10Z")

</div>

I’m currently working on a plugin to Obsidian to upload blog-posts to ghost. And with that I also want it to upload any pictures used in that document. I’ve spent about 30 hours on this now with a little progress but would need some help to try and speed things along. It was just yesterday I think I bypassed the CORS errors I’ve been having with nginx.

Back to the issue in hand - I now get 500 errors when sending to the API - I suspect I’ve constructed the formdata incorrectly. All information can be seen bellow:

Source:

```ts
			// Building the image data
			let imageData;
			if (frontmatter.imageDirectory) {
				const imageNamePrefix = frontmatter.imageDirectory.replace(/\//g, "");
				imageData = {
					"images": [
						{
							"url": `${BASE_URL}/content/images/${year}/${month}/${imageNamePrefix}-${filename}`,
							"ref": imagePath
						}
					]
				}
			} else {
				imageData = {
					"images": [
						{
							"url": `${BASE_URL}/content/images/${year}/${month}/${filename}`,
							"ref": imagePath
						}
					]
				}
			}
			console.log("imagedata", imageData);
			
			// Make blob https://developer.mozilla.org/en-US/docs/Web/API/Blob
			const blob = new Blob([JSON.stringify(imageData)], {
				type: "application/json",
			});

			// Construct formdata of blob
			const formData = new FormData();
			formData.append("file", blob);
			formData.append("purpose", "image");
			formData.append("ref", imagePath);

			try {
				const response = await fetch(`${settings.url}/ghost/api/${version}/admin/images/upload/`, {
					method: "POST",
					headers: {
						"Content-Type": "multipart/form-data",
						Authorization: `Ghost ${token}`,	
					},
					body: formData
				});
				if (response.ok) {
					// Handle success
					const data = await response.json();
					console.log(data);
				} else {
					// Handle errors
					console.error("Error:", response.statusText);
					console.error("Error:", response.statusText);
					console.error("Status Code:", response.status); // Add status code
					console.error("Response Headers:", response.headers); // Log response headers
					response.text().then(errorText => {
						console.error("Error Response Text:", errorText); // Log the response body as text
					}).catch(error => {
						console.error("Error parsing response text:", error);
					});
				}
				} catch (error) {
					console.error("Request error:", error);
				}
			}

```

Request headers:

```auto
:Authority:
mydomain.com
:Method:
POST
:Path:
/ghost/api/v4/admin/images/upload/
:Scheme:
https
Accept:
*/*
Accept-Encoding:
gzip, deflate, br
Accept-Language:
en-US
Authorization:
Ghost <MY-TOKEN>
Content-Length:
580
Content-Type:
multipart/form-data
Origin:
app://obsidian.md
Sec-Ch-Ua-Mobile:
?0
Sec-Ch-Ua-Platform:
"Linux"
Sec-Fetch-Mode:
cors
Sec-Fetch-Site:
cross-site
User-Agent:
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.4.5 Chrome/114.0.5735.289 Electron/25.8.0 Safari/537.36

```

Response headers:

```auto
Access-Control-Allow-Headers:
Authorization,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range
Access-Control-Allow-Methods:
GET, POST, OPTIONS
Access-Control-Allow-Origin:
*
Access-Control-Expose-Headers:
Content-Length,Content-Range
Alt-Svc:
h3=":443"; ma=86400
Cache-Control:
no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0
Cf-Cache-Status:
DYNAMIC
Cf-Ray:
8097d87c0b84190d-FRA
Content-Length:
280
Content-Type:
application/json; charset=utf-8
Content-Version:
v5.49
Date:
Wed, 20 Sep 2023 06:00:00 GMT
Deprecation:
version="v4"
Etag:
W/"118-TO41uly8SaFcUo6z2CfflFSlP3I"
Link:
<https://mydomain.com/ghost/api/admin/images/upload/>; rel="latest-version"
Nel:
{"success_fraction":0,"report_to":"cf-nel","max_age":604800}
Report-To:
{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=nUEexVPgiZM6W0xtL2SDloBn1%2F%2FmZeisLhIh1Dbgx0PZvlofdEvKRI6w1dU9quzeeFjcEQzNis7YSs1XhAmS1rHKlOv9zKdG8Q7lSdoRtJoHekCB%2FjKWLErdqDH9L6rmfEuPuXJRD8nC"}],"group":"cf-nel","max_age":604800}
Server:
cloudflare
Vary:
Accept-Version, Accept-Encoding
X-Powered-By:
Express

```

Console error message:

```auto
Error Response Text: {"errors":[{"message":"An unexpected error occurred, please try again.","context":"Multipart: Boundary not found","type":"InternalServerError","details":null,"property":null,"help":null,"code":"UNEXPECTED_ERROR","id":"ef416110-577a-11ee-ad51-8f5ac464e714","ghostErrorCode":null}]}

```

Thank you for any help!

---

<div class="post-metadata">

**Author:** ![markstos](https://sea1.discourse-cdn.com/flex015/user_avatar/forum.ghost.org/markstos/32/18182_2.png) [@markstos](https://forum.ghost.org/u/markstos)\
**Post date:** [September 21, 2023, 2:38pm UTC](https://forum.ghost.org/t/uploading-images-to-ghost-admin-api/41348/2 "2023-09-21T14:38:32Z")

</div>

Your code is confusing the upload request format, which is “form data”, with the response format, which is in JSON.

It appears your code is trying to upload images using the response format.

I’m reviewing the API documentation here:

> **[Ghost Admin API Documentation](https://ghost.org/docs/admin-api/#images)**
>
> Manage content via Ghost's Admin API, with secure role-based authentication. Read more on Ghost Docs 👉

It provides a working example with `curl`

> curl -X POST -F ‘file=@/path/to/images/my-image.jpg’ -F ‘ref=path/to/images/my-image.jpg’ -H “Authorization: ‘Ghost $token’” -H “Accept-Version: $version” https://{admin\_domain}/ghost/api/admin/images/upload/

That’s submitting a `file` field and an optional `ref` field in the “form data” format, along with an Authorization header. That’s all that JavaScript would need to do as well.

Another good sourced of example JavaScript for working with Ghost can be the Ghost test suite itself. For example, here’s a test for image uploading:

> <https://github.com/TryGhost/Ghost/blob/098f4353a7a50ced3b55acf569738731258cb30b/ghost/core/test/e2e-api/admin/images.test.js#L27>

It’s using an `agent` object like superagent, but you could use another library for the upload. Here’s the key code:

```javascript
const form = new FormData();
    form.append('file', fileContents, {
        filename,
        contentType
    });

    form.append('purpose', 'image');
    if (ref) {
        form.append('ref', ref);
    }

    return agent
        .post('/images/upload/')
        .body(form);

```

I hope that helps!

---

<div class="post-metadata">

**Author:** ![Hailst0rm](https://sea1.discourse-cdn.com/flex015/user_avatar/forum.ghost.org/hailst0rm/32/26198_2.png) [@Hailst0rm](https://forum.ghost.org/u/Hailst0rm)\
**Post date:** [September 21, 2023, 5:08pm UTC](https://forum.ghost.org/t/uploading-images-to-ghost-admin-api/41348/3 "2023-09-21T17:08:59Z")

</div>

Thank you so much for the comment!

You seem to be right - I’ve confused the request data with the expected response. But I still seem to be getting the same error (still assuming it’s because of the formdata somehow). In regards to the code segments you sent me - I’ve played around a bit but still no luck.

From what I understand `fileContents` is a `fs.readFile(absolutePathToImage)`, `filename` is just the name of the file and `contentType` is ex `image/png`. With this approach (see code below) I get a warning `Expected 2-3 arguments, but got 1.ts(2554)` from the `fs.readFile` as if it’s acting differently? If I try to execute it anyways I get this error `caught (in promise) TypeError: The "cb" argument must be of type function. Received undefined at __node_internal_captureLargerStackTrace (`.

Code:

```ts
			const fileContent = await fs.readFile(imagePath);
			const contentType = "image/png"
			const formData = new FormData();
			formData.append("file", fileContent, {
				filename,
				contentType
			});
			formData.append("purpose", "image");
			formData.append("ref", imagePath);

			try {
				const response = await fetch(`${settings.url}/ghost/api/${version}/admin/images/upload/`, {
					method: "POST",
					headers: {
						"Content-Type": "multipart/form-data",
						Authorization: `Ghost ${token}`,	
					},
					body: formData
				});
                               // Error handling etc...

```

Also when I try this: `formData.append("file", fs.createReadStream(absolutePathToImage))` which I’ve also seen, I get a complaint of it not being a Blob.

I’m very grateful for any additional help!

---

<div class="post-metadata">

**Author:** ![markstos](https://sea1.discourse-cdn.com/flex015/user_avatar/forum.ghost.org/markstos/32/18182_2.png) [@markstos](https://forum.ghost.org/u/markstos)\
**Post date:** [September 21, 2023, 5:24pm UTC](https://forum.ghost.org/t/uploading-images-to-ghost-admin-api/41348/4 "2023-09-21T17:24:55Z")

</div>

Now it sounds to me like you are getting tripped on the difference between the old and new `fs` APIs, which both have a function named `readFile()`.

[https://nodejs.org/api/fs.html](https://nodejs.org/api/fs.html)

You didn’t show how you imported `fs`, but my guess is that’s where the problem is, and you should have something like one of these:

```javascript
import * as fs from 'node:fs/promises';
// Or if you are using `require()` statements
const fs = require('node:fs/promises');

```

Now `fs` will have the promise-based API that you expect.

---

<div class="post-metadata">

**Author:** ![markstos](https://sea1.discourse-cdn.com/flex015/user_avatar/forum.ghost.org/markstos/32/18182_2.png) [@markstos](https://forum.ghost.org/u/markstos)\
**Post date:** [September 21, 2023, 5:25pm UTC](https://forum.ghost.org/t/uploading-images-to-ghost-admin-api/41348/5 "2023-09-21T17:25:43Z")

</div>

> [@Hailst0rm](#):
>
> `/ghost/api/${version}/admin/images/upload/`

Oh, it it seems like a mistake that the $version is included in the URL here. I don’t see that in the official docs.

---

<div class="post-metadata">

**Author:** ![Hailst0rm](https://sea1.discourse-cdn.com/flex015/user_avatar/forum.ghost.org/hailst0rm/32/26198_2.png) [@Hailst0rm](https://forum.ghost.org/u/Hailst0rm)\
**Post date:** [September 21, 2023, 6:02pm UTC](https://forum.ghost.org/t/uploading-images-to-ghost-admin-api/41348/6 "2023-09-21T18:02:21Z")

</div>

Since I’m using typescript I import it now with `import * as fs from 'fs/promises';` which seems to work well and makes the output of `fs.readFile` a buffer (as in the example above). But according to the ghost documentation it wants a Blob or File - and so I get the error `TypeError: Failed to execute 'append' on 'FormData': parameter 2 is not of type 'Blob'.`. I also tried to make it a blob but haven’t gotten that to work either (same 500 error as before):

```ts
			const fileContent = await fs.readFile(imagePath);
			const contentType = "image/png"
			const fileBlob = new Blob([fileContent], { type: contentType });
			const formData = new FormData();
			formData.append("file", fileBlob);
			formData.append("purpose", "image");
			formData.append("ref", imagePath);

```

I also tried removing the version from the url, both seems to “work” just as well, it was there in the code I got inspired by which is why it was there to begin with.

---

<div class="post-metadata">

**Author:** ![markstos](https://sea1.discourse-cdn.com/flex015/user_avatar/forum.ghost.org/markstos/32/18182_2.png) [@markstos](https://forum.ghost.org/u/markstos)\
**Post date:** [September 21, 2023, 6:18pm UTC](https://forum.ghost.org/t/uploading-images-to-ghost-admin-api/41348/7 "2023-09-21T18:18:01Z")

</div>

You should not need to do the extra blob conversion. Look how the file is handled in the the test suite:

> <https://github.com/TryGhost/Ghost/blob/098f4353a7a50ced3b55acf569738731258cb30b/ghost/core/test/e2e-api/admin/images.test.js#L57>

The only thing that’s done is that is `await fs.readFile()` is called on it.

Then it’s passed to `form.append('file, fileContents)`:

> <https://github.com/TryGhost/Ghost/blob/098f4353a7a50ced3b55acf569738731258cb30b/ghost/core/test/e2e-api/admin/images.test.js#L29>

---

<div class="post-metadata">

**Author:** ![Hailst0rm](https://sea1.discourse-cdn.com/flex015/user_avatar/forum.ghost.org/hailst0rm/32/26198_2.png) [@Hailst0rm](https://forum.ghost.org/u/Hailst0rm)\
**Post date:** [September 21, 2023, 6:39pm UTC](https://forum.ghost.org/t/uploading-images-to-ghost-admin-api/41348/8 "2023-09-21T18:39:20Z")

</div>

Yeah that’s what I tried… but I get complaints when I run it :/

```ts
			const fileContent = await fs.readFile(imagePath);
			const contentType = "image/png"
			const formData = new FormData();
			formData.append("file", fileContent, {
				filename,
				contentType
			});
			formData.append("purpose", "image");
			formData.append("ref", imagePath);

			try {
				const response = await fetch(`${settings.url}/ghost/api/admin/images/upload/`, {
					method: "POST",
					headers: {
						"Content-Type": "multipart/form-data",
						Authorization: `Ghost ${token}`,	
					},
					body: formData
				});

```

Error:

```js
Uncaught (in promise) TypeError: Failed to execute 'append' on 'FormData': parameter 2 is not of type 'Blob'.
    at eval (VM1417 plugin:obsidian-ghost-publish:21562:18)
    at Generator.next (<anonymous>)
    at fulfilled (VM1417 plugin:obsidian-ghost-publish:50:24)

```

---

<div class="post-metadata">

**Author:** ![markstos](https://sea1.discourse-cdn.com/flex015/user_avatar/forum.ghost.org/markstos/32/18182_2.png) [@markstos](https://forum.ghost.org/u/markstos)\
**Post date:** [September 22, 2023, 2:08am UTC](https://forum.ghost.org/t/uploading-images-to-ghost-admin-api/41348/9 "2023-09-22T02:08:50Z")

</div>

Have you considered using the official Ghost JavaScript SDK? With it, you don’t have to worry about content types, formData or blobs. It would be like this:

```javascript
import GhostAdminAPI from '@tryghost/admin-api'

const token = 'SECRET';

const api = new GhostAdminAPI({
  url: 'http://localhost:2368',
  key: token,
  version: "v5.0",
});

const apiRes = await api.images.upload({file: imagePath});

console.log(apiRes);

```

It’s documented here:

> **[Admin API JavaScript Client](https://ghost.org/docs/admin-api/javascript/#working-example)**
>
> JavaScript Client Library for making requests to the Ghost Admin API using API keys. Read more on Ghost Docs 👉

---

<div class="post-metadata">

**Author:** ![Hailst0rm](https://sea1.discourse-cdn.com/flex015/user_avatar/forum.ghost.org/hailst0rm/32/26198_2.png) [@Hailst0rm](https://forum.ghost.org/u/Hailst0rm)\
**Post date:** [September 22, 2023, 6:05am UTC](https://forum.ghost.org/t/uploading-images-to-ghost-admin-api/41348/10 "2023-09-22T06:05:56Z")

</div>

Yeah I wish it was that easy, I’ve tried it both before and after I solved my CORS issues with nginx. However due to the plugins use of axios I still get CORS Issues:

Code: `const response = await api.images.upload({ref: imagePath, file: imagePath});`  
Error(s):

```auto
Access to XMLHttpRequest at 'https://mydomain.com/ghost/api/v4/admin/images/upload/' from origin 'app://obsidian.md' has been blocked by CORS policy: Request header field accept-version is not allowed by Access-Control-Allow-Headers in preflight response.
xhr.js:251 
 POST https://admin.hailstormsec.com/ghost/api/v4/admin/images/upload/ net::ERR_FAILED
plugin:obsidian-ghost-publish:59 Uncaught (in promise) 
AxiosError {message: 'Network Error', name: 'AxiosError', code: 'ERR_NETWORK', stack: 'AxiosError: Network Error\n at XMLHttpRequest.handleError (plugin:obsidian-ghost-publish:20125:18)'}

```

This is the background to why I am using fetch to begin with - it both supports sending formdata and enabled me to bypass CORS in a viable manner. But then the issues above hit me instead (which they shouldn’t?).

So yes… still at trying to construct a request something the server will accept 😕

---

<div class="post-metadata">

**Author:** ![markstos](https://sea1.discourse-cdn.com/flex015/user_avatar/forum.ghost.org/markstos/32/18182_2.png) [@markstos](https://forum.ghost.org/u/markstos)\
**Post date:** [September 22, 2023, 12:49pm UTC](https://forum.ghost.org/t/uploading-images-to-ghost-admin-api/41348/11 "2023-09-22T12:49:26Z")

</div>

I see.

One thing I see that’s unlikely to be the root case is the call to `formData.append()`. The latest docs show the final argument is a single filename, not an object:

> **[FormData: append() method - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/FormData/append)**
>
> The append() method of the FormData interface appends a new value onto an existing key inside a FormData object, or adds the key if it does not already exist.

According do the [docs for Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob), it an can be an array of TypedArrays.

According to the [docs for fs.readFile()](https://nodejs.org/api/fs.html#fspromisesreadfilepath-options), that method returns a promise which resolves to an object of type Buffer.

The docs also say that [Buffer instances are also TypedArray instances](https://nodejs.org/api/buffer.html#buffers-and-typedarrays) but with some minor differences.

So far, so good.

It says if you want to completely convert a Buffer to a TypedArray, you could do this:

```auto
const uint32array = new Uint32Array(buf);

```

You could try that.

---

<div class="post-metadata">

**Author:** ![Hailst0rm](https://sea1.discourse-cdn.com/flex015/user_avatar/forum.ghost.org/hailst0rm/32/26198_2.png) [@Hailst0rm](https://forum.ghost.org/u/Hailst0rm)\
**Post date:** [September 24, 2023, 8:01am UTC](https://forum.ghost.org/t/uploading-images-to-ghost-admin-api/41348/12 "2023-09-24T08:01:02Z")

</div>

Yeah it didn’t wok too well either…

```ts
			const fileContent = await fs.readFile(imagePath);
			// 1. const uint32array = new Uint32Array(fileContent);
			// 2. const blob = new Blob([fileContent]);

			const formData = new FormData();
			// 1. formData.append("file", uint32array);
			// 2. formData.append("file", blob, filename);
			formData.append("purpose", "image");
			formData.append("ref", imagePath);

			try {
				const response = await fetch(`${settings.url}/ghost/api/admin/images/upload/`, {
					method: "POST",
					headers: {
						"Content-Type": "multipart/form-data",
						Authorization: `Ghost ${token}`,	
					},
					body: formData
				});

```

1: What you suggested, gave the same error about not being a blob.  
2: Same 500 error as before - also a combination of 1 and 2 gave this error. I managed to get the entire error. I managed to get the entire error log from my server - not that I think it has more useful information (?):

```auto
{"name":"Log","hostname":"home-server","pid":969,"level":50,"version":"5.49.0","req":{"meta":{"requestId":"09d027e4-b4a1-4871-8809-63a3686bf571","userId":null},"url":"/images/upload/","method":"POST","originalUrl":"/ghost/api/admin/images/upload/","params":{},"headers":{"x-forwarded-for":"83.250.20.209, 172.71.102.36","x-forwarded-proto":"https","x-real-ip":"172.71.102.36","host":"admin.mydoman.com","connection":"close","content-length":"14843","cf-connecting-ip":"83.250.20.209","cf-ipcountry":"SE","accept-encoding":"gzip","cf-ray":"80b965854f0ed0d9-AMS","cf-visitor":"{\"scheme\":\"https\"}","accept":"*/*","accept-language":"en-GB","authorization":" **REDACTED**","content-type":"multipart/form-data","origin":"app://obsidian.md","sec-fetch-mode":"cors","sec-fetch-site":"cross-site","user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.3.5 Chrome/112.0.5615.183 Electron/24.3.1 Safari/537.36","sec-ch-ua-mobile":"?0","sec-ch-ua-platform":"\"Windows\"","priority":"u=1, i","cdn-loop":"cloudflare"},"query":{}},"res":{"_headers":{"x-powered-by":"Express","content-version":"v5.49","vary":"Accept-Version, Accept-Encoding","cache-control":"no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0","content-type":"application/json; charset=utf-8","content-length":"280","etag":"W/\"118-+DBQysdUY8GtoJMEA2/L49ROf58\""},"statusCode":500,"responseTime":"10ms"},"err":{"id":"0c864700-5aae-11ee-bdce-0fb68b067b1d","domain":"https://mydoman.com","code":"UNEXPECTED_ERROR","name":"InternalServerError","statusCode":500,"level":"critical","message":"An unexpected error occurred, please try again.","context":"\"Multipart: Boundary not found\"","stack":"Error: Multipart: Boundary not found\n at module.exports.prepareError (/var/www/mydoman/versions/5.49.0/node_modules/@tryghost/mw-error-handler/lib/mw-error-handler.js:92:19)\n at new Multipart (/var/www/mydoman/versions/5.49.0/node_modules/busboy/lib/types/multipart.js:58:11)\n at Multipart (/var/www/mydoman/versions/5.49.0/node_modules/busboy/lib/types/multipart.js:26:12)\n at Busboy.parseHeaders (/var/www/mydoman/versions/5.49.0/node_modules/busboy/lib/main.js:71:22)\n at new Busboy (/var/www/mydoman/versions/5.49.0/node_modules/busboy/lib/main.js:22:10)\n at multerMiddleware (/var/www/mydoman/versions/5.49.0/node_modules/multer/lib/make-middleware.js:33:16)\n at /var/www/mydoman/versions/5.49.0/core/server/web/api/middleware/upload.js:53:5\n at Layer.handle [as handle_request] (/var/www/mydoman/versions/5.49.0/node_modules/express/lib/router/layer.js:95:5)\n at next (/var/www/mydoman/versions/5.49.0/node_modules/express/lib/router/route.js:144:13)\n at notImplemented (/var/www/mydoman/versions/5.49.0/core/server/web/api/endpoints/admin/middleware.js:51:20)\n at Layer.handle [as handle_request] (/var/www/mydoman/versions/5.49.0/node_modules/express/lib/router/layer.js:95:5)\n at next (/var/www/mydoman/versions/5.49.0/node_modules/express/lib/router/route.js:144:13)\n at uncapitalise (/var/www/mydoman/versions/5.49.0/core/server/web/shared/middleware/uncapitalise.js:60:5)\n at Layer.handle [as handle_request] (/var/www/mydoman/versions/5.49.0/node_modules/express/lib/router/layer.js:95:5)\n at next (/var/www/mydoman/versions/5.49.0/node_modules/express/lib/router/route.js:144:13)\n at slashes (/var/www/mydoman/versions/5.49.0/node_modules/connect-slashes/lib/connect-slashes.js:81:9)\n at Layer.handle [as handle_request] (/var/www/mydoman/versions/5.49.0/node_modules/express/lib/router/layer.js:95:5)","hideStack":false},"msg":"An unexpected error occurred, please try again.","time":"2023-09-24T07:43:27.603Z","v":0}

```

I just now realised looking at this forum that theres a boundary header of some sort - which is also mentioned in the error message… I’ll see if that can do something for me, and you (or anyone else) know how to before I get back here; feel free to post the solution. [Error Uploading Images with Admin API: 'Please select an image.' - #3 by gjdickens](https://forum.ghost.org/t/error-uploading-images-with-admin-api-please-select-an-image/21616/3)

---

<div class="post-metadata">

**Author:** ![markstos](https://sea1.discourse-cdn.com/flex015/user_avatar/forum.ghost.org/markstos/32/18182_2.png) [@markstos](https://forum.ghost.org/u/markstos)\
**Post date:** [September 24, 2023, 1:56pm UTC](https://forum.ghost.org/t/uploading-images-to-ghost-admin-api/41348/13 "2023-09-24T13:56:06Z")

</div>

You may need to _not_ set the `Content-Type` header in this case. This is explained in the [MDN docs on FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects)

Here’s that warning:

> **Warning:** When using FormData to submit POST requests using [`XMLHttpRequest`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) or the [`Fetch_API`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) with the `multipart/form-data` Content-Type (e.g. when uploading Files and Blobs to the server), _do not_ explicitly set the [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) header on the request. Doing so will prevent the browser from being able to set the Content-Type header with the boundary expression it will use to delimit form fields in the request body.

---

<div class="post-metadata">

**Author:** ![markstos](https://sea1.discourse-cdn.com/flex015/user_avatar/forum.ghost.org/markstos/32/18182_2.png) [@markstos](https://forum.ghost.org/u/markstos)\
**Post date:** [September 25, 2023, 12:15am UTC](https://forum.ghost.org/t/uploading-images-to-ghost-admin-api/41348/14 "2023-09-25T00:15:46Z")

</div>

@Hailst0rm Here’s code I wrote that works for me. I confirmed it returns a successful response and that the file is indeed on the file system after the upload.

Here I don’t send the `Content-Type` as I advised above.

```javascript
import * as fs from 'node:fs/promises';

// Construct the token from the key exactly as Ghost would.
import keyToToken from '@tryghost/admin-api/lib/token.js'
const token = keyToToken(key, '/admin');

const key = 'ADMIN-KEY-FROM-GHOST';

const imagePath = '/home/mark/Downloads/face.png';
const filename = 'face.png';
const settings = {
  url: 'http://127.0.0.1:2368',
};
 
const fileContent = await fs.readFile(imagePath);
			const contentType = "image/png"
			const fileBlob = new Blob([fileContent], { type: contentType });
			const formData = new FormData();
			formData.append("file", fileBlob, filename);
			formData.append("purpose", "image");
			formData.append("ref", imagePath);

      let response;
			try {
				response = await fetch(`${settings.url}/ghost/api/admin/images/upload/`, {
					method: "POST",
					headers: {
						// "Content-Type": "multipart/form-data", // Don't include!
						Authorization: `Ghost ${token}`,	
					},
					body: formData
				});
     }
     catch (err) {
        console.error("ERROR",err);
     };

     console.log(await response?.json()); 

```

That produces this output for me:

```json
{
  images: [
    {
      url: 'http://localhost:2368/content/images/2023/09/face.png',
      ref: '/home/mark/Downloads/face.png'
    }
  ]
}

```

---

<div class="post-metadata">

**Author:** ![markstos](https://sea1.discourse-cdn.com/flex015/user_avatar/forum.ghost.org/markstos/32/18182_2.png) [@markstos](https://forum.ghost.org/u/markstos)\
**Post date:** [September 25, 2023, 12:23am UTC](https://forum.ghost.org/t/uploading-images-to-ghost-admin-api/41348/15 "2023-09-25T00:23:35Z")

</div>

While reading the source code of the `admin-api` module I gained some insight into why some API URLs have versions in them in some don’t.

It said that has of Ghost 5, including version numbers in the URLs is being phased out. The comment is:

> This method can go away in favor of only sending 'Accept-Version` headers once the Ghost API removes a concept of version from it’s URLS (with Ghost v5)

So to future-proof your code, send the `Accept-Version` header with `v5 as the value, but leave the version out of the URL.

---

<div class="post-metadata">

**Author:** ![Hailst0rm](https://sea1.discourse-cdn.com/flex015/user_avatar/forum.ghost.org/hailst0rm/32/26198_2.png) [@Hailst0rm](https://forum.ghost.org/u/Hailst0rm)\
**Post date:** [September 25, 2023, 6:53am UTC](https://forum.ghost.org/t/uploading-images-to-ghost-admin-api/41348/16 "2023-09-25T06:53:22Z")

</div>

Removing the header was the final piece of the puzzle - thank you so much for all the help! I just had to play around with making it a blob again (like your code) and it all worked out. For anyone interested here’s the final code:

```auto
			let imagePath = `${imageDirectory}/${result[1]}`;
			let filename = result[1];

			// If extended directory - add image prefix
			if (frontmatter.imageDirectory) {
				filename = `${frontmatter.imageDirectory.replace(/\//g, "")}-${filename}`;
			}

			// Get the image data buffer
			const fileContent = await fs.readFile(imagePath);

			// Determine the file type based on the filename's extension
			const fileExtension = filename.split('.').pop();
			let fileType = '';

			if (fileExtension === 'png') {
			fileType = 'image/png';
			} else if (fileExtension === 'jpeg' || fileExtension === 'jpg') {
			fileType = 'image/jpeg';
			} // Add more file types if needed

			// Make blob of buffer to allow formdata.append
			const blob = new Blob([fileContent], { type: fileType });

			console.log("filename", filename);
			// console.log("imagePath", imagePath);
			const formData = new FormData();
			formData.append("file", blob, filename);
			formData.append("purpose", "image");
			formData.append("ref", filename);

			try {
				const response = await fetch(`${settings.url}/ghost/api/${version}/admin/images/upload/`, {
					method: "POST",
					headers: {
						Authorization: `Ghost ${token}`,
					},
					body: formData
				});
				if (response.ok) {
					// Handle success
					const data = await response.json();
					console.log(data);
				} else {
					// Handle errors
					console.error("Error:", response.statusText);
					console.error("Error:", response.statusText);
					console.error("Status Code:", response.status); // Add status code
					console.error("Response Headers:", response.headers); // Log response headers
					response.text().then(errorText => {
						console.error("Error Response Text:", errorText); // Log the response body as text
					}).catch(error => {
						console.error("Error parsing response text:", error);
					});
				}
				} catch (error) {
					console.error("Request error:", error);
				}
			}
	}

```
