Codebusters

Instagram Access Token Expiring? How to Check and Refresh It from Cron

Wednesday 7 October 2026

If you post to Instagram from a script, you’ll have a token that’s supposed to expire after 60 days, and every guide tells you to refresh it from cron. I did exactly that, with a script that runs every Monday. Then I looked at my log and found it saying “Expires in 0 days” every single week, along with a PHP warning about a missing expires_in. The posts were still going out. So what was going on?

Check what kind of token you actually have

Facebook has an endpoint, debug_token, that tells you what a token is and when it ends. This is the little script I use to ask it:

$ig = parse_ini_file('/home/me/config/secrets.ini', true)['instagram'];

$url = 'https://graph.facebook.com/v21.0/debug_token?' . http_build_query([
    'input_token'  => $ig['ig_access_token'],
    'access_token' => $ig['app_id'] . '|' . $ig['app_secret'],
]);
$info = json_decode(file_get_contents($url), true)['data'] ?? [];

echo 'Type:    ' . ($info['type'] ?? '?') . "\n";
echo 'Valid:   ' . (!empty($info['is_valid']) ? 'yes' : 'NO') . "\n";
$exp = $info['expires_at'] ?? null;
echo 'Expires: ' . ($exp ? date('j M Y', $exp) : 'never') . "\n";

On mine it prints:

Type:    PAGE
Valid:   yes
Expires: never

That explains the log. My token is a Page access token, which is what you typically get when a long-lived user token is used to fetch the token for a Facebook Page (the Page that’s linked to the Instagram account). Page tokens made that way don’t expire at all, so there is no expires_in in the reply, and my script was dividing nothing by 86,400 and printing 0.

If yours says USER and gives a date, you have the 60-day kind, and the refresh below matters. Check which you’ve got before you worry!

The refresh script

This is the same approach as my weekly script. It exchanges the current token for a new one, then saves it back into the .ini file where all my keys live, changing only that one line:

$secretsFile = '/home/me/config/secrets.ini';
$ini = parse_ini_file($secretsFile, true);
$ig  = $ini['instagram'];

// 1. Ask Facebook for a fresh long-lived token
$url = 'https://graph.facebook.com/v21.0/oauth/access_token?' . http_build_query([
    'grant_type'        => 'fb_exchange_token',
    'client_id'         => $ig['app_id'],
    'client_secret'     => $ig['app_secret'],
    'fb_exchange_token' => $ig['ig_access_token'],
]);
$response = json_decode(file_get_contents($url), true);

if (!isset($response['access_token'])) {
    error_log('Token refresh failed: ' . json_encode($response));
    exit(1);
}

// 2. Save it back into the ini file, changing only that one line
$contents = file_get_contents($secretsFile);
$contents = preg_replace_callback(
    '/^ig_access_token\s*=.*$/m',
    fn () => 'ig_access_token = ' . $response['access_token'],
    $contents,
    1
);
file_put_contents($secretsFile, $contents, LOCK_EX);

// 3. Not every token comes back with an expiry, so don't assume
$days = isset($response['expires_in']) ? round($response['expires_in'] / 86400) . ' days' : 'no expiry given';
echo date('Y-m-d H:i:s') . " Token refreshed OK ($days)\n";

I couldn’t run that against Instagram without risking my own live token, so I tested it in two other ways. First I pointed it at a fake token server on my own machine, and it fetched a new token and wrote it into the file. Second I tested the file-saving part on its own, and that found a mistake!

My first version looked for a line beginning access_token. My test file had one of those in two different sections, and it changed both. If you have other keys with the same name, you’d quietly overwrite the wrong one. So give the Instagram key a name nobody else will use (I call mine ig_access_token), and the 1 at the end limits it to a single replacement.

I also used preg_replace_callback() rather than preg_replace(). With the ordinary one, a token containing something like $1 is treated as a back-reference. I tried it, and the $1 simply vanished from the token. With the callback version, a token containing both $1 and a backslash came out untouched.

Running it from cron

Every Monday at 3am:

0 3 * * 1 /usr/bin/php /home/me/scripts/refresh_instagram_token.php >> /home/me/logs/instagram_token_refresh.log 2>&1

Once a week is more than enough for a 60-day token. The log line it writes is the important bit, because if the script ever starts failing you’ll have about 8 weeks’ notice. You’ll want to see a failure in the log and not discover it when your posts stop.

What I’d check first if posts stop working

  1. Run the debug_token script above and see whether Valid says yes
  2. If it says no, look at the log for the last time the refresh worked
  3. If the token has already expired, it can’t be refreshed. You’ll need to generate a new one by hand and put it in the file

I’d much rather find out from a log file on a Monday morning than from a post that never appeared!