Skip to content

Diagnostic API and Integration

Admin or integration tokens expire and silently break automation

A script or cron job calls POST /rest/V1/integration/admin/token once, saves the bearer token in an env var, and runs fine for hours. Then every REST call starts coming back Unauthorized, and because a 401 is a plain HTTP status rather than a business exception, the automation often swallows it instead of alerting anyone. Here is why the admin token quietly expires, and a script that detects the failure, re-authenticates safely once, and reports the rest.

Python and Node.js Magento REST API Detect, refresh once, report
A stack of papers
Photo by Kelly Sikkema on Unsplash
The short answer

An admin token from POST /rest/V1/integration/admin/token is a session style bearer token with a default lifetime of only four hours, controlled by Stores, Configuration, Services, OAuth, Access Token Expiration, Admin Token Lifetime, and an hourly cron job purges expired tokens from the admin_bearer_token table. A script that hardcodes the token once works fine until it expires, then every call fails with a 401 that is easy to swallow silently. Run a small Python or Node.js script that probes the credential against a cheap endpoint like GET /V1/store/storeConfigs, classifies the failure with a pure decision function, re-authenticates exactly once on a genuine expiry, and stops and reports on anything else, including the risk of tripping the account lockout. The full code and a dry run guard are below.

The problem in plain words

Calling POST /rest/V1/integration/admin/token with a username and password is the fastest way to get a bearer token in every tutorial, so it is also the way most scripts get wired up first. The token works, the script runs, and nobody thinks about it again.

But that token was never meant to live forever. It behaves like a login session, and by default Magento gives it four hours before it expires, a value read from Stores, Configuration, Services, OAuth, Access Token Expiration, Admin Token Lifetime. On top of that, an hourly cron job actively purges expired rows from the admin_bearer_token table, so there is nothing left to fall back on even by accident. A cron job or script that cached the token in an environment variable or a config file keeps sending the same string, and once the clock runs out, every call gets rejected with HTTP 401 and a message like "The consumer isn't authorized to access %resources" or a generic Unauthorized reason. Because that is a plain HTTP status and not a business exception, it is easy for automation to log it and move on instead of raising an alert.

Script gets admin token caches it once, in env var Works for 4 hours default Admin Token Lifetime cron purges the token row admin_bearer_token row deleted, expired Every call 401 Unauthorized The 401 is a plain HTTP status, not a business exception the automation is watching for. Scripts that only log errors swallow it, and the automation quietly stops working.
The token is not revoked on purpose. It simply reached the end of its configured lifetime, and cron already cleaned up the row that would have proven it.

Why it happens

This is one of the most common Magento integration complaints once a script has been running quietly for a few weeks. See the citations at the end for the exact support thread and forum discussions.

The key insight

The admin token was never meant to be a long lived credential, it is a login session with a clock attached. So the fix is not to hunt for a longer expiry setting or to refresh on a fixed timer that might drift from the real configuration. It is to treat every 401 as a signal to classify, not just retry: was this a normal expiry that is safe to refresh once, or a revoked or invalid credential that should stop the script and get a human's attention before it risks an account lockout.

The fix, as a flow

We do not touch the live checkout or admin session. We add a probe that calls a cheap, side effect free endpoint before the real work, reads the HTTP status and message, and classifies the result. A genuine expiry gets one automatic re-authentication and one retry. Anything else, including a second failure right after refreshing, stops the script and reports it instead of hammering the login endpoint.

GET /V1/store/storeConfigs cheap probe with cached token classifyTokenFailure status, message, issued at, now EXPIRED_REAUTH or other? expired Re-auth once POST admin/token, retry request revoked, invalid, or lockout risk Stop and report, do not retry loop avoids tripping the admin lockout
Only a genuine, in-lifetime expiry gets an automatic refresh and one retry. Anything else stops the script so a human can look at it, rather than hammering the login endpoint.

Build it step by step

1

Set up credentials and pick your durable path

Get an admin token by calling POST {'{'}MAGENTO_URL{'}'}/rest/V1/integration/admin/token with your admin username and password, or better, create a long lived Integration under System, Extensions, Integrations with explicit API resource ACLs and activate it, since that token never expires on its own. Keep the base URL, admin credentials, and token in environment variables, never in the file.

setup (shell)
pip install requests

export MAGENTO_URL="https://yourstore.example.com"
export MAGENTO_ADMIN_TOKEN="eyJraWQ..."
export MAGENTO_ADMIN_USER="automation-user"
export MAGENTO_ADMIN_PASS="change-me"
export ADMIN_TOKEN_LIFETIME_HOURS="4"
export DRY_RUN="true"   # start safe, change to false to allow the re-auth call
setup (shell)
// Node 18+ has fetch built in, no dependencies needed

export MAGENTO_URL="https://yourstore.example.com"
export MAGENTO_ADMIN_TOKEN="eyJraWQ..."
export MAGENTO_ADMIN_USER="automation-user"
export MAGENTO_ADMIN_PASS="change-me"
export ADMIN_TOKEN_LIFETIME_HOURS="4"
export DRY_RUN="true"   // start safe, change to false to allow the re-auth call
2

Talk to the Magento REST API

Every call sends the cached token as a bearer header. A small helper wraps GET requests, returns the status code and parsed body instead of raising immediately, so the caller can inspect a 401 before deciding what to do about it.

step2.py
import os, requests

MAGENTO_URL = os.environ["MAGENTO_URL"].rstrip("/")

def get_with_status(path, token, params=None):
    r = requests.get(
        f"{MAGENTO_URL}/rest/V1{path}",
        params=params or {},
        headers={"Authorization": f"Bearer {token}"},
        timeout=30,
    )
    try:
        body = r.json()
    except ValueError:
        body = {}
    return r.status_code, body
step2.js
const MAGENTO_URL = (process.env.MAGENTO_URL || "").replace(/\/$/, "");

async function getWithStatus(path, token, params = {}) {
  const url = new URL(`${MAGENTO_URL}/rest/V1${path}`);
  for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
  const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
  let body = {};
  try {
    body = await res.json();
  } catch {
    body = {};
  }
  return { status: res.status, body };
}
3

Probe with a cheap, side-effect-free endpoint

Call GET /V1/store/storeConfigs with the cached token. It reads nothing sensitive and writes nothing, so it is safe to call before every real batch of work. A 200 means the token is fine. A 401 means either it expired or it was revoked, and we need the classifier to tell those apart.

step3.py
def probe_token(token):
    status, body = get_with_status("/store/storeConfigs", token)
    return status, body
step3.js
async function probeToken(token) {
  const { status, body } = await getWithStatus("/store/storeConfigs", token);
  return { status, body };
}
4

Decide, with one pure function

Keep the classification in its own function, with no I/O, so it is easy to read and easy to test. It takes the HTTP status, the response body, when the token was issued, the current time, and the configured lifetime, and returns one of four outcomes. A 401 within the configured lifetime is treated as revoked or invalid, not expiry, so the script does not blindly refresh a credential that was deliberately cut off.

decide.py
def classify_token_failure(http_status, response_body, token_issued_at, now, configured_lifetime_hours, retry_count=0, retry_threshold=1):
    if http_status == 200:
        return "OK"
    if http_status == 401:
        age_hours = (now - token_issued_at).total_seconds() / 3600
        if retry_count >= retry_threshold:
            return "LOCKOUT_RISK"
        if age_hours >= configured_lifetime_hours:
            return "EXPIRED_REAUTH"
        return "REVOKED_OR_INVALID"
    return "REVOKED_OR_INVALID"
decide.js
export function classifyTokenFailure(httpStatus, responseBody, tokenIssuedAt, now, configuredLifetimeHours, retryCount = 0, retryThreshold = 1) {
  if (httpStatus === 200) return "OK";
  if (httpStatus === 401) {
    const ageHours = (now - tokenIssuedAt) / 3600000;
    if (retryCount >= retryThreshold) return "LOCKOUT_RISK";
    if (ageHours >= configuredLifetimeHours) return "EXPIRED_REAUTH";
    return "REVOKED_OR_INVALID";
  }
  return "REVOKED_OR_INVALID";
}
5

Re-authenticate exactly once on a genuine expiry

When the classifier says EXPIRED_REAUTH, call POST /V1/integration/admin/token with the admin username and password to mint a fresh token, replace the cached token, and retry the original request exactly once. Do not loop. If the re-auth call itself fails, or the retried request fails again, that is a sign of something worse than plain expiry.

apply.py
def get_new_admin_token(username, password):
    r = requests.post(
        f"{MAGENTO_URL}/rest/V1/integration/admin/token",
        json={"username": username, "password": password},
        timeout=30,
    )
    if r.status_code != 200:
        raise RuntimeError(f"Re-authentication failed with status {r.status_code}")
    return r.json()
apply.js
async function getNewAdminToken(username, password) {
  const res = await fetch(`${MAGENTO_URL}/rest/V1/integration/admin/token`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ username, password }),
  });
  if (res.status !== 200) throw new Error(`Re-authentication failed with status ${res.status}`);
  return res.json();
}
6

Wire it together with a dry run guard

The loop probes first, classifies the result, and only acts on EXPIRED_REAUTH. Notice the dry run guard. On the first few runs, leave DRY_RUN on so the script only reports what it would do. Anything classified REVOKED_OR_INVALID or LOCKOUT_RISK stops the run and logs the job name, timestamp, and 401 payload instead of retrying, since repeatedly hammering the token endpoint with bad credentials risks Magento's admin account lockout threshold.

Run it safe

Never loop on a 401. Re-authenticate at most once per run, only when the classifier says EXPIRED_REAUTH, and stop and report on anything else. The long term fix is a permanent Integration token created under System, Extensions, Integrations with explicit ACLs, since it removes the four hour clock entirely.

The full code

Here is the complete script in one file for each language. It reads settings from the environment, probes the cached token against a cheap endpoint, classifies any failure with a pure function, re-authenticates exactly once on a genuine expiry, and logs and stops on anything else.

Get this script on GitHub Follow @allanninal Python and Node.js, with tests. Dry run by default. One of 59 Magento fixes, free and open source.
detect_token_expiry.py
"""Detect a silently expired or revoked Magento admin token and recover safely.

POST /rest/V1/integration/admin/token returns a session style bearer token with a
default four hour lifetime (Admin Token Lifetime under Stores, Configuration,
Services, OAuth, Access Token Expiration), and an hourly cron purges expired rows
from admin_bearer_token. A script that caches the token once works fine until it
expires, then every call fails with a plain HTTP 401 that is easy to swallow silently.

This script probes a cheap, side-effect-free endpoint, classifies the result with a
pure function, and re-authenticates exactly once on a genuine expiry. Anything else,
including a repeated failure right after refreshing, stops the run and reports it
instead of looping, since repeated bad logins risk tripping the admin account lockout.
"""
import os
import logging
import datetime
import requests

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("detect_token_expiry")

MAGENTO_URL = os.environ["MAGENTO_URL"].rstrip("/")
ADMIN_USER = os.environ.get("MAGENTO_ADMIN_USER", "")
ADMIN_PASS = os.environ.get("MAGENTO_ADMIN_PASS", "")
LIFETIME_HOURS = float(os.environ.get("ADMIN_TOKEN_LIFETIME_HOURS", "4"))
DRY_RUN = os.environ.get("DRY_RUN", "true").lower() == "true"

RETRY_THRESHOLD = 1


def classify_token_failure(http_status, response_body, token_issued_at, now, configured_lifetime_hours, retry_count=0, retry_threshold=RETRY_THRESHOLD):
    if http_status == 200:
        return "OK"
    if http_status == 401:
        age_hours = (now - token_issued_at).total_seconds() / 3600
        if retry_count >= retry_threshold:
            return "LOCKOUT_RISK"
        if age_hours >= configured_lifetime_hours:
            return "EXPIRED_REAUTH"
        return "REVOKED_OR_INVALID"
    return "REVOKED_OR_INVALID"


def get_with_status(path, token, params=None):
    r = requests.get(
        f"{MAGENTO_URL}/rest/V1{path}",
        params=params or {},
        headers={"Authorization": f"Bearer {token}"},
        timeout=30,
    )
    try:
        body = r.json()
    except ValueError:
        body = {}
    return r.status_code, body


def probe_token(token):
    return get_with_status("/store/storeConfigs", token)


def get_new_admin_token(username, password):
    r = requests.post(
        f"{MAGENTO_URL}/rest/V1/integration/admin/token",
        json={"username": username, "password": password},
        timeout=30,
    )
    if r.status_code != 200:
        raise RuntimeError(f"Re-authentication failed with status {r.status_code}")
    return r.json()


def run():
    token = os.environ.get("MAGENTO_ADMIN_TOKEN", "")
    token_issued_at = datetime.datetime.fromisoformat(
        os.environ.get("TOKEN_ISSUED_AT", datetime.datetime.now(datetime.timezone.utc).isoformat())
    )
    retry_count = 0

    while True:
        now = datetime.datetime.now(datetime.timezone.utc)
        status, body = probe_token(token)
        outcome = classify_token_failure(status, body, token_issued_at, now, LIFETIME_HOURS, retry_count, RETRY_THRESHOLD)

        if outcome == "OK":
            log.info("Token is valid. Automation can proceed.")
            return

        if outcome == "EXPIRED_REAUTH":
            log.warning("Token expired after its configured lifetime. %s",
                        "Would re-authenticate." if DRY_RUN else "Re-authenticating.")
            if DRY_RUN:
                return
            new_token = get_new_admin_token(ADMIN_USER, ADMIN_PASS)
            token = new_token if isinstance(new_token, str) else new_token.get("token", token)
            token_issued_at = now
            retry_count += 1
            continue

        if outcome == "REVOKED_OR_INVALID":
            log.error("Job admin-token-expiry-breaks-automation: token rejected while still within its "
                      "configured lifetime at %s. 401 payload: %s. Flagging for manual review, not retrying.",
                      now.isoformat(), body)
            return

        log.error("Job admin-token-expiry-breaks-automation: repeated failure at %s after a refresh attempt. "
                  "Stopping to avoid the admin account lockout. 401 payload: %s", now.isoformat(), body)
        return


if __name__ == "__main__":
    run()
detect-token-expiry.js
/**
 * Detect a silently expired or revoked Magento admin token and recover safely.
 *
 * POST /rest/V1/integration/admin/token returns a session style bearer token with a
 * default four hour lifetime (Admin Token Lifetime under Stores, Configuration,
 * Services, OAuth, Access Token Expiration), and an hourly cron purges expired rows
 * from admin_bearer_token. A script that caches the token once works fine until it
 * expires, then every call fails with a plain HTTP 401 that is easy to swallow silently.
 *
 * This script probes a cheap, side-effect-free endpoint, classifies the result with a
 * pure function, and re-authenticates exactly once on a genuine expiry. Anything else
 * stops the run and reports it instead of looping, since repeated bad logins risk
 * tripping the admin account lockout.
 *
 * Guide: https://www.allanninal.dev/magento/admin-token-expiry-breaks-automation/
 */
import { pathToFileURL } from "node:url";

const MAGENTO_URL = (process.env.MAGENTO_URL || "https://example.test").replace(/\/$/, "");
const ADMIN_USER = process.env.MAGENTO_ADMIN_USER || "";
const ADMIN_PASS = process.env.MAGENTO_ADMIN_PASS || "";
const LIFETIME_HOURS = Number(process.env.ADMIN_TOKEN_LIFETIME_HOURS || 4);
const DRY_RUN = (process.env.DRY_RUN || "true").toLowerCase() === "true";

const RETRY_THRESHOLD = 1;

export function classifyTokenFailure(httpStatus, responseBody, tokenIssuedAt, now, configuredLifetimeHours, retryCount = 0, retryThreshold = RETRY_THRESHOLD) {
  if (httpStatus === 200) return "OK";
  if (httpStatus === 401) {
    const ageHours = (now - tokenIssuedAt) / 3600000;
    if (retryCount >= retryThreshold) return "LOCKOUT_RISK";
    if (ageHours >= configuredLifetimeHours) return "EXPIRED_REAUTH";
    return "REVOKED_OR_INVALID";
  }
  return "REVOKED_OR_INVALID";
}

async function getWithStatus(path, token, params = {}) {
  const url = new URL(`${MAGENTO_URL}/rest/V1${path}`);
  for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
  const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
  let body = {};
  try {
    body = await res.json();
  } catch {
    body = {};
  }
  return { status: res.status, body };
}

async function probeToken(token) {
  return getWithStatus("/store/storeConfigs", token);
}

async function getNewAdminToken(username, password) {
  const res = await fetch(`${MAGENTO_URL}/rest/V1/integration/admin/token`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ username, password }),
  });
  if (res.status !== 200) throw new Error(`Re-authentication failed with status ${res.status}`);
  return res.json();
}

export async function run() {
  let token = process.env.MAGENTO_ADMIN_TOKEN || "";
  let tokenIssuedAt = process.env.TOKEN_ISSUED_AT ? Date.parse(process.env.TOKEN_ISSUED_AT) : Date.now();
  let retryCount = 0;

  while (true) {
    const now = Date.now();
    const { status, body } = await probeToken(token);
    const outcome = classifyTokenFailure(status, body, tokenIssuedAt, now, LIFETIME_HOURS, retryCount, RETRY_THRESHOLD);

    if (outcome === "OK") {
      console.log("Token is valid. Automation can proceed.");
      return;
    }

    if (outcome === "EXPIRED_REAUTH") {
      console.warn(`Token expired after its configured lifetime. ${DRY_RUN ? "Would re-authenticate." : "Re-authenticating."}`);
      if (DRY_RUN) return;
      const newToken = await getNewAdminToken(ADMIN_USER, ADMIN_PASS);
      token = typeof newToken === "string" ? newToken : newToken.token || token;
      tokenIssuedAt = now;
      retryCount += 1;
      continue;
    }

    if (outcome === "REVOKED_OR_INVALID") {
      console.error(
        `Job admin-token-expiry-breaks-automation: token rejected while still within its configured lifetime at ${new Date(now).toISOString()}. 401 payload: ${JSON.stringify(body)}. Flagging for manual review, not retrying.`
      );
      return;
    }

    console.error(
      `Job admin-token-expiry-breaks-automation: repeated failure at ${new Date(now).toISOString()} after a refresh attempt. Stopping to avoid the admin account lockout. 401 payload: ${JSON.stringify(body)}`
    );
    return;
  }
}

if (import.meta.url === pathToFileURL(process.argv[1]).href) {
  run().catch((err) => { console.error(err); process.exit(1); });
}

Add a test

classify_token_failure is the part worth testing, because it decides whether the script refreshes quietly, stops and reports, or backs off entirely. It is pure decision logic with no I/O, so the test needs no network and no Magento store. It just feeds in plain values and checks the answer.

test_admin_classify.py
import datetime
from detect_token_expiry import classify_token_failure

ISSUED = datetime.datetime(2026, 7, 10, 8, 0, tzinfo=datetime.timezone.utc)


def test_ok_on_200():
    now = ISSUED + datetime.timedelta(hours=1)
    assert classify_token_failure(200, {}, ISSUED, now, 4) == "OK"


def test_expired_reauth_after_lifetime():
    now = ISSUED + datetime.timedelta(hours=5)
    assert classify_token_failure(401, {"message": "Unauthorized"}, ISSUED, now, 4) == "EXPIRED_REAUTH"


def test_revoked_when_within_lifetime():
    now = ISSUED + datetime.timedelta(hours=1)
    body = {"message": "The consumer isn't authorized to access %resources"}
    assert classify_token_failure(401, body, ISSUED, now, 4) == "REVOKED_OR_INVALID"


def test_exactly_at_lifetime_is_expired():
    now = ISSUED + datetime.timedelta(hours=4)
    assert classify_token_failure(401, {}, ISSUED, now, 4) == "EXPIRED_REAUTH"


def test_lockout_risk_when_retry_threshold_hit():
    now = ISSUED + datetime.timedelta(hours=5)
    assert classify_token_failure(401, {}, ISSUED, now, 4, retry_count=1, retry_threshold=1) == "LOCKOUT_RISK"


def test_non_401_non_200_treated_as_revoked_or_invalid():
    now = ISSUED + datetime.timedelta(hours=1)
    assert classify_token_failure(500, {}, ISSUED, now, 4) == "REVOKED_OR_INVALID"
classify.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { classifyTokenFailure } from "./detect-token-expiry.js";

const ISSUED = Date.parse("2026-07-10T08:00:00Z");
const hoursLater = (h) => ISSUED + h * 3600000;

test("OK on 200", () => {
  assert.equal(classifyTokenFailure(200, {}, ISSUED, hoursLater(1), 4), "OK");
});

test("EXPIRED_REAUTH after lifetime", () => {
  const body = { message: "Unauthorized" };
  assert.equal(classifyTokenFailure(401, body, ISSUED, hoursLater(5), 4), "EXPIRED_REAUTH");
});

test("REVOKED_OR_INVALID when within lifetime", () => {
  const body = { message: "The consumer isn't authorized to access %resources" };
  assert.equal(classifyTokenFailure(401, body, ISSUED, hoursLater(1), 4), "REVOKED_OR_INVALID");
});

test("exactly at lifetime is expired", () => {
  assert.equal(classifyTokenFailure(401, {}, ISSUED, hoursLater(4), 4), "EXPIRED_REAUTH");
});

test("LOCKOUT_RISK when retry threshold hit", () => {
  assert.equal(classifyTokenFailure(401, {}, ISSUED, hoursLater(5), 4, 1, 1), "LOCKOUT_RISK");
});

test("non 401 non 200 treated as revoked or invalid", () => {
  assert.equal(classifyTokenFailure(500, {}, ISSUED, hoursLater(1), 4), "REVOKED_OR_INVALID");
});

Case studies

Overnight ERP sync

The sync job that quietly stopped syncing

A mid sized retailer had a nightly ERP sync that pulled orders through the Magento REST API using an admin token fetched once when the job was first written, months earlier, and cached in a deployment secret. The token had actually been rotated by an unrelated deploy weeks ago, so every night's sync had been failing with 401s, logged as warnings that nobody read.

Adding the probe and classifier caught the very next run: REVOKED_OR_INVALID fired immediately since the cached token was nowhere near a normal four hour age, which correctly pointed at a credential problem rather than plain expiry. That distinction sent the team straight to checking what had rotated the token, instead of chasing a retry loop that would never have fixed it.

Hourly price feed

An hourly job that ran into the four hour wall

A pricing integration polled Magento every hour using a token minted once at deploy time, and worked fine for the first several runs. On the fifth run, past the default four hour Admin Token Lifetime, cron had already purged the token, and the job started failing silently, its error handling only logged a generic "sync failed" line.

Once the classifier was in place, that failure came back as EXPIRED_REAUTH, the script refreshed the token exactly once, retried, and kept going without anyone touching it. The team then moved the integration onto a permanent Integration token so the refresh step became unnecessary going forward.

What good looks like

Run before every batch of work, this probe turns a silent, hours later failure into an immediate, specific outcome: automatic recovery for a normal expiry, or a clear stop-and-report the moment a credential is genuinely revoked or invalid. Nobody has to guess why a job "just stopped working" three weeks in. And once the durable Integration token is in place, the whole detection loop becomes a safety net rather than something you rely on daily.

FAQ

Why does my Magento REST script start failing after a few hours?

An admin token from POST /rest/V1/integration/admin/token is a session style bearer token with a default lifetime of four hours, set under Stores, Configuration, Services, OAuth, Access Token Expiration, Admin Token Lifetime. An hourly cron job purges expired tokens from the admin_bearer_token table, so a script that hardcoded the token once keeps sending it and every call after expiry gets rejected with a 401, often silently if the automation does not check the status code.

What is the difference between an admin token and an integration token?

An admin token comes from POST /rest/V1/integration/admin/token using a username and password, and it expires after the configured Admin Token Lifetime, four hours by default. An integration token comes from an Integration created under System, Extensions, Integrations with explicit API resource ACLs, and once activated it does not expire on its own, it only stops working if someone revokes or deactivates it. Integrations are the durable choice for unattended automation.

Is it safe for a script to automatically get a new admin token on a 401?

Yes, once, and only for that specific failure. Classify the 401 first: if the token is older than the configured lifetime, it is a normal expiry and re-authenticating once with POST /rest/V1/integration/admin/token is safe. If the token is still within its lifetime and still gets a 401, that means the credentials or the integration were revoked, and the script should stop and report instead of retrying, because repeated bad login attempts risk tripping Magento's admin account lockout.

Related field notes

Citations

On the problem:

  1. Finale Inventory support: access token expiring frequently on a Magento connection. support.finaleinventory.com Access-token-expiring-frequently-on-Magento-connection
  2. Magento Forums: limited access for OAuth Authentication. community.magento.com Limited-access-for-OAuth-Authentication
  3. Magento 2 GitHub issue: OAuth1.0 request token request failing, consumer key has expired. github.com/magento/magento2/issues/829

On the solution:

  1. Adobe Commerce developer docs: Token-Based Authentication. developer.adobe.com/commerce/webapi/get-started/authentication/gs-authentication-token
  2. Adobe Commerce developer docs: Tutorial, Step 2, get the admin token. developer.adobe.com/commerce/webapi/rest/tutorials/orders/order-admin-token
  3. Adobe Commerce developer docs: Prerequisite Tasks, generate the admin token. developer.adobe.com/commerce/webapi/rest/tutorials/prerequisite-tasks

Stuck on a tricky one?

If you have a problem in Magento integrations, cron, indexing, or order and inventory sync that you would rather hand off, this is the kind of work I do. Message me and we can work through it together.

Contact me on LinkedIn

Did this save your automation from a silent 401?

If this saved you hours of chasing a job that "just stopped working," you can buy me a coffee. It is the best way to keep these field notes free and growing.

Buy me a coffee on Ko-fi

Back to all Magento field notes