Cannot Import Website Content JSON file via API

Hello and thank you for any help you may be able to provide.

I am using the latest ghost, it is installed in a subfolder, setup is on digital ocean droplet.

When I import my custom .json file (it contains posts, post-tag relationships - tags are already present in the system, and single user) via the admin dashboard ui it works great.

But for my use-case I need to be able to do this programatically.

I have been trying to do the following (with my information):

curl -X POST “https://example.com/blog/ghost/api/admin/db/
-H “Authorization: Ghost YOUR_ADMIN_API_KEY”
-F “importfile=@/absolute/path/to/import.json”

But I get this error:

{“errors”:[{“message”:“Invalid token”,“context”:null,“type”:“BadRequestError”,“details”:null,“property”:null,“help”:null,“code”:“INVALID_JWT”,“id”:“7e59a7d0-04d7-11f0-86fc-cf66509cf4ac”,“ghostErrorCode”:null}]}

I have verified that my domain and subfolder are correct, and my admin api token is correct (I also created a new one to test with and I got the same error), and my path to my .json file is correct.

Thank you so much for your help - I have been trying to get this to work but am having a challenging time.

You need to generate a JWT from your admin API key - you can’t directly send the API key. Here are the relevant docs:

You need not the API key, but a token. See this section - particularly on authentication and generating a token. Ghost Admin API Documentation

laughing @vikaspotluri123 we are just too fast! :laughing:

1 Like

Thank you so much for your help and speedy response @vikaspotluri123 and @Cathy_Sarisky!

Ok I used the following to generate the needed token and attempt to upload the .json file:

const fs = require('fs');
const jwt = require('jsonwebtoken');
const axios = require('axios');
const FormData = require('form-data');
const path = require('path');

const GHOST_URL = "https://yourdomain.com/folder";  // 🔹 Replace with your Ghost URL
const ADMIN_API_KEY = "your_admin_api_key";  // 🔹 Replace with your actual Admin API Key
const IMPORT_FILE_PATH = "/absolute/path/to/import.json";  // 🔹 Replace with your actual file path

// 🔴 Ensure Required Variables Are Set
if (!GHOST_URL || !ADMIN_API_KEY || !IMPORT_FILE_PATH) {
    console.error("❌ Missing required values. Update the script with your Ghost URL, Admin API Key, and JSON file path.");
    process.exit(1);
}

// ✅ Extract API Key ID and Secret
const [id, secret] = ADMIN_API_KEY.split(':');
if (!id || !secret) {
    console.error("❌ Invalid Admin API Key format. Expected format: {id}:{secret}");
    process.exit(1);
}

// ✅ Generate JWT Token
const createJwtToken = () => {
    const now = Math.floor(Date.now() / 1000);
    return jwt.sign(
        { exp: now + 300, iat: now, aud: "/admin/" },  // Payload
        Buffer.from(secret, 'hex'),
        { keyid: id, algorithm: "HS256", header: { typ: "JWT" } }
    );
};

// ✅ Upload JSON to Ghost Admin API
const uploadJsonToGhost = async () => {
    try {
        console.log(`📂 Reading import file from: ${IMPORT_FILE_PATH}`);

        if (!fs.existsSync(IMPORT_FILE_PATH)) {
            throw new Error(`❌ File not found: ${IMPORT_FILE_PATH}`);
        }

        const token = createJwtToken();
        console.log(`🔑 Generated JWT Token`);

        const apiUrl = `${GHOST_URL}/ghost/api/admin/db/`;
        const formData = new FormData();
        formData.append("importfile", fs.createReadStream(IMPORT_FILE_PATH));

        console.log(`📡 Uploading import.json to ${apiUrl}...`);

        const response = await axios.post(apiUrl, formData, {
            headers: {
                Authorization: `Ghost ${token}`,
                ...formData.getHeaders(),
            },
        });

        console.log("✅ Import successful!", response.data);
    } catch (error) {
        console.error("❌ Import failed:", error.response?.data || error.message);
    }
};

// ✅ Run the Upload Function
uploadJsonToGhost();

The token is generated and upload is attempted; however, I got this error:

❌ Import failed: {
  errors: [
    {
      message: 'You do not have permission to importContent db',
      context: null,
      type: 'NoPermissionError',
      details: null,
      property: null,
      help: null,
      code: null,
      id: 'b0e96b20-04e3-11f0-86fc-cf66509cf4ac',
      ghostErrorCode: null
    }
  ]
}

Am I able to upload a single .json file in this way? Before I was attempting to read my .json file and upload the posts and tag associations via the admin api (
api.posts.add … ), but that only uploaded the posts and did not retain the tag associations.

What I am trying to solve is this: I need to programmatically upload posts such that the tag-post associations are retained. My hope was that I could achieve this by uploading the single .json file in one go because when I programmatically do this for each post added via admin api, a new id is set such that it is no longer the id in my .json file and so the tag associations are lost. Manually uploading works great and tag associations are retained then, but again I need to do this via the terminal for my use-case.

Let me know if that makes sense or if I can given further information to clarify.

Thank you again for your help!

I’d take a look at the exact network call being made when you upload through the dashboard. Any differences?

Thank you - when manually uploading I am seeing the Request URL is the same, the Request Method also POST, and Status Code is 200 ok (it is 403 for when I try to do this programmatically). (Not sure if this helps)

I thought maybe I needed to run this script on the digital ocean server where ghost is installed, so I did that, but the same permission error occurred.

However, I solved my issue by returning to using the admin api, but adjusting my json file so that the tags would simply be part of the post object instead of a separate post-tag object - a relation that broke due to the forced creation of a new id on import.

Thank you again for your help today!