TryGhost/Ghost · error · NotFoundError

ApiKey not found.

Error message

ApiKey not found.

What it means

A NotFoundError from `IntegrationsService.edit` when `options.keyid` is set and `ApiKeyModel.findOne({id: options.keyid})` returns no model. This path refreshes an integration's API-key secret; if the supplied key id does not exist the service refuses to proceed. It does not check the key's integration ownership beyond existence.

Source

Thrown at ghost/core/core/server/services/integrations/integrations-service.js:19

const {NotFoundError, InternalServerError} = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');

const messages = {
    notFound: '{resource} not found.'
};

class IntegrationsService {
    constructor({IntegrationModel, ApiKeyModel}) {
        this.IntegrationModel = IntegrationModel;
        this.ApiKeyModel = ApiKeyModel;
    }

    async edit(data, options) {
        if (options.keyid) {
            const model = await this.ApiKeyModel.findOne({id: options.keyid});

            if (!model) {
                throw new NotFoundError({
                    message: tpl(messages.notFound, {
                        resource: 'ApiKey'
                    })
                });
            }
            try {
                await this.ApiKeyModel.refreshSecret(model.toJSON(), Object.assign({}, options, {id: options.keyid}));

                return await this.IntegrationModel.findOne({id: options.id}, {
                    withRelated: ['api_keys', 'webhooks']
                });
            } catch (err) {
                throw new InternalServerError({
                    err: err
                });
            }
        }

View on GitHub (pinned to 47d8b0e2ad)

Solutions

  1. Re-fetch the integration's current API keys (`GET /integrations/{id}/?include=api_keys`) and use a live `keyid`.
  2. Confirm the `keyid` belongs to the integration being edited.
  3. Remove the `keyid` param if you only want to edit integration metadata, not refresh a secret.
  4. If the key truly no longer exists, create a new API key for the integration instead of refreshing a dead one.

Example fix

// before
await integrations.edit({id}, {keyid: staleKeyId, ...}); // -> NotFoundError

// after
const fresh = await integrations.findOne({id}, {withRelated: ['api_keys']});
const keyid = fresh.api_keys[0].id;
await integrations.edit({id}, {keyid});
Defensive patterns

Strategy: validation

Validate before calling

async function assertApiKeyExists(apiKeyModel, keyid) {
  const m = await apiKeyModel.findOne({id: keyid});
  if (!m) throw new Error(`ApiKey ${keyid} not found; refresh the integration's keys`);
  return m;
}

Type guard

const apiKeyExists = async (model, id) => Boolean(await model.findOne({id}));

Try / catch

try {
  await integrations.edit({id}, {keyid});
} catch (err) {
  if (err.type === 'NotFoundError' && /ApiKey not found/i.test(err.message)) refreshApiKeys();
  else throw err;
}

Prevention

When it happens

Trigger: PUT to edit an integration with a `keyid` parameter referring to an API key id that was deleted, never existed, or belongs to a different integration. The `require`-less `findOne` returns null and the service throws before attempting the secret refresh.

Common situations: The API key was rotated/deleted in a separate request before this call; the client cached a stale key id; the key id was copy-pasted incorrectly (truncated/wrong field); an integration was recreated and old key ids are invalid.

Related errors


AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13). Data as JSON: /api/errors/fc8a4f664d369368. Report an issue: GitHub.