Blog

QuickBooks Online Tokens Failing After 24 Hours in Quickbase Pipelines: A Refresh Pattern That Keeps Working

invalid_grant at the 24-hour mark means an old refresh token was sent. Keep both tokens in one Quickbase record, let a single scheduled pipeline refresh them and write both back every run, and have every other pipeline just read the access token. Includes setup, error handling and a checklist.

Stop the 24-hour invalid_grant error with a QuickBooks Online token refresh pipeline in Quickbase

A question in the Quickbase community described a problem that a lot of people hit when they connect Pipelines to QuickBooks Online with their own API calls (original thread here). They had an hourly pipeline refreshing their QuickBooks OAuth 2.0 tokens, plus a second pipeline doing the same thing every 12 hours to be safe. Everything worked for about a day, and then the refresh call started failing with invalid_grant.

The short answer: an old refresh token is being sent. Intuit changes the refresh token about once a day, and the old one stops working as soon as the new one is handed out. Every run has to save both tokens it gets back and read them again next time, and only one pipeline should ever do the refreshing.

This guide builds that setup from scratch: a small table that holds the tokens, one scheduled refresh pipeline, a pattern for the pipelines that actually talk to QuickBooks, and a troubleshooting list for when you do see invalid_grant.

One QBO Connection record holds the tokens. A single scheduled pipeline reads the refresh token, calls Intuit, and writes both tokens back. Every other pipeline only reads the access token.

First, check whether you need any of this

Pipelines has a built-in QuickBooks Online channel. You connect your QuickBooks account once in the step's Connection section and Pipelines handles the tokens for you. It has triggers and steps for customers, vendors, invoices, bills, items, estimates, expenses, credit memos, time activities and payments.

If you're doing standard things like creating customers or invoices, use the channel and skip the rest of this guide. You only need your own token handling when you call the QuickBooks API directly with a Make Request step. Quickbase's own help mentions a couple of cases where that's necessary, such as sub-customers and the Rate field on invoice lines, which the channel doesn't currently support.

How Intuit's tokens actually behave

These come from Intuit's developer documentation and support articles:

  • Access token: good for one hour. The token response says so with "expires_in": 3600. You send it on every API call as Authorization: Bearer <access token>. A 401 from the API usually means it has expired.
  • Refresh token: used only to get a new access token. It's valid for 100 days on a rolling basis, and each refresh pushes that out again. The response includes x_refresh_token_expires_in (about 8,640,000 seconds) so you can see it.
  • The refresh token rotates. For roughly the first day, a refresh gives you back the same refresh token. After about 24 to 26 hours, the next refresh returns a new one, and once a new refresh token is returned the previous one is expired. Intuit's advice is to always store the latest access token and refresh token from every response, even when the refresh token looks unchanged.
  • Sending an old refresh token returns 400 with invalid_grant. That's the error from the thread.
  • There's also a hard limit now. Intuit announced that refresh tokens have a maximum life of five years. For apps using the standard accounting or payments scopes, the first of those tokens start expiring in October 2028, so it isn't urgent, but it does mean you'll eventually have to reconnect even when everything is working.

That rotation is why the failure shows up at the 24-hour mark and not right away. A stale copy of the refresh token keeps working while Intuit is still handing back the same value. Then a refresh returns a new token, and whatever is still holding the old copy starts failing.

In the thread's setup, the hourly pipeline and the 12-hour pipeline each had their own idea of the current refresh token. As soon as one of them got a rotated token, the other one's copy was dead.

The pattern

Three rules make this reliable:

  1. One place for the tokens. A single Quickbase record holds the current access token and refresh token. Nothing else keeps a copy, and no tokens get pasted into step fields.
  2. One refresher. Exactly one pipeline calls Intuit's token endpoint. It reads the refresh token from the record and writes both new tokens back on every run.
  3. Everyone else only reads. Pipelines that create invoices or look up customers read the access token from the record and never refresh it themselves.

Step 1: Create the token table

Create a table called something like QBO Connection. It will only ever have one record (or one per QuickBooks company, if you connect more than one). Add these fields:

FieldTypeWhat goes in it
Connection NameTextA label, such as "Main company"
Realm IDTextThe QuickBooks company ID. API URLs need it
Access TokenText - Multi-lineThe current access token. These are long, so don't use a short text field
Refresh TokenTextThe current refresh token
Access Token Expires AtDate / TimeWhen the access token runs out
Refresh Token Expires AtDate / TimeThe rolling 100-day expiry, for reference
Last Refreshed AtDate / TimeWhen the last successful refresh ran
Last Refresh ResultTextOK, or a short note when a refresh fails

Then add two formula fields so you can see at a glance whether it's healthy.

Token Status (Formula - Text):

If(
  IsNull([Access Token Expires At]), "Not connected",
  Left([Last Refresh Result], 2) <> "OK", "Refresh failed - check it",
  [Access Token Expires At] < Now(), "Access token expired",
  [Access Token Expires At] < Now() + Minutes(10), "Expiring soon",
  "OK"
)

Minutes Left (Formula - Numeric):

Floor(ToMinutes([Access Token Expires At] - Now()))

I built this table in a sandbox app with placeholder token values to check the formulas. This is what they showed for three test situations:

Connection NameLast Refresh ResultMinutes LeftToken Status
Main companyOK45OK
Late scheduled runOK1Expiring soon
After a failed refreshFailed - see activity log-33Refresh failed - check it

Lock the table down. Anyone who can see this record can use your QuickBooks connection. In Roles, give only admins access to the table, and leave it off forms, reports and dashboards that regular users see. Don't put the client secret in this table at all. That goes in the pipeline step (Step 3).

Step 2: Get the first set of tokens

The pipeline can only keep a connection going. Something has to create it first. If you already have a working refresh token, put it in the record and skip to Step 3. If yours has already failed with invalid_grant, you're in the same spot as someone starting from scratch: you need one fresh authorization.

The simplest way is Intuit's OAuth 2.0 Playground in the Intuit developer portal:

  1. Sign in to the Intuit developer portal and open the OAuth 2.0 Playground.
  2. Pick your app, and choose development (sandbox) or production keys to match the company you're connecting.
  3. Select the accounting scope and authorize. Sign in as a QuickBooks user for the company and approve the connection.
  4. Exchange the authorization code for tokens. The Playground shows the realm ID, access token and refresh token.
  5. Paste those into your QBO Connection record: Realm ID, Access Token, Refresh Token. Set Access Token Expires At to an hour from now and Last Refresh Result to OK.

Watch for stray spaces or line breaks when you paste. A refresh token with a trailing space is a different token as far as Intuit is concerned.

Make sure the tokens come from the same app (client ID and secret) the pipeline will use. A refresh token only works with the app that issued it, and sandbox and production keys are different apps for this purpose.

Step 3: Build the refresh pipeline

This is the only pipeline that will ever call the token endpoint.

  1. In Pipelines, select Create Pipeline and give it a clear name, like QBO - Refresh tokens (only refresher). Don't add a trigger. You'll put it on a schedule at the end.

  2. Step A: Quickbase > Look Up Record. Point it at the QBO Connection table and look up the record by Record ID# (for example, record 1). The rest of the pipeline uses this step's Refresh Token field.

  3. Step B: Webhooks > Make Request. Fill it in like this:

    • URL: https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer

    • Method: POST

    • Content type: application/x-www-form-urlencoded

    • Headers: add one header named Accept with the value application/json, so the response comes back as JSON

    • Authentication: choose Basic. Put your app's client ID in Username and its client secret in Password. Intuit expects the client ID and secret as a Basic authorization header on this call.

    • Body:

      grant_type=refresh_token&refresh_token={{a.refresh_token}}
      

      Instead of typing {{a.refresh_token}}, drag the Refresh Token field in from step A's outputs. The exact name depends on your field label, and dragging it in gets it right.

  4. Step C: Quickbase > Update Record. Update the same QBO Connection record and map:

    FieldValue
    Access Token{{b.json.access_token}}
    Refresh Token{{b.json.refresh_token}}
    Access Token Expires At{{time.now + time.delta(seconds=b.json.expires_in)}}
    Refresh Token Expires At{{time.now + time.delta(seconds=b.json.x_refresh_token_expires_in)}}
    Last Refreshed At{{time.now}}
    Last Refresh ResultOK

    The Refresh Token line is the one that matters. Write it on every run, not only when it looks different. Most of the time the value is the same as what's already there. Once a day it isn't, and that's the run you can't afford to miss.

  5. Schedule it. Select Schedule pipeline and set it to run every 1 hour. That's the shortest interval Pipelines allows. Scheduled runs start within a few minutes of the set time.

Here's what a healthy record looks like across a day (values shortened):

RunRefresh token sentRefresh token returnedSaved to record
9:00 AMRT1-...a41RT1-...a41 (same)RT1-...a41
10:00 AMRT1-...a41RT1-...a41 (same)RT1-...a41
... next day, 9:00 AMRT1-...a41RT1-...c97 (new)RT1-...c97
10:00 AMRT1-...c97RT1-...c97 (same)RT1-...c97

The failing setups break on the row where the token changes. If anything still sends ...a41 after that run, Intuit answers invalid_grant.

Step 4: Record failures instead of losing them

If the refresh call fails, you want to know before your invoices stop syncing. Pipelines has a Handle errors block for this:

  1. Add a Handle errors block and move steps B and C inside it, between Start monitoring and Stop monitoring.
  2. On the On error path, add a Quickbase > Update Record step on the same record that sets Last Refresh Result to something like Failed - see activity log. Don't touch the token fields.
  3. If you want an alert, add a notification step (an email or a message to your team's channel) on the same path.

Don't clear or overwrite the stored tokens when a refresh fails. Intuit says invalid_grant can also come back during an outage or maintenance, and in that case the stored refresh token is still good and the next hourly run will work. Blanking the record would turn a temporary problem into a reconnect.

There's one gap to know about: if step B succeeds and step C fails, Intuit may have issued a new refresh token that never got saved. Keep step C simple. Don't put required fields, unique fields or data rules on this table that could reject the update, and check the activity log if you ever see the update fail.

Step 5: Have every other pipeline read the access token

Your pipelines that actually work with QuickBooks follow the same pattern:

  1. Whatever trigger you need (a record is created, a checkbox is ticked, and so on).

  2. Quickbase > Look Up Record on QBO Connection to get Access Token and Realm ID.

  3. Webhooks > Make Request to the QuickBooks API:

    • URL: https://quickbooks.api.intuit.com/v3/company/{{b.realm_id}}/query for production. Sandbox companies use https://sandbox-quickbooks.api.intuit.com instead.
    • Method: POST
    • Content type: application/text
    • Headers: Authorization = Bearer {{b.access_token}} and Accept = application/json
    • Body: a QuickBooks query, for example select * from Customer where DisplayName = 'Mike Chen'

    (Step letters depend on where the Look Up Record step sits, so drag the fields in rather than copying the letters above.)

  4. Read the result from the request step's JSON output, the same way as in the refresh pipeline ({{c.json...}}).

These pipelines never call the token endpoint. If you find yourself adding a refresh to one of them "just in case," that's exactly the second refresher that causes the 24-hour failure.

The hourly gap. Access tokens last 60 minutes, and the refresh runs about every 60 minutes, so there can be a few minutes where the stored access token has just expired and the next refresh hasn't run yet. A call in that window gets a 401. Two ways to handle it:

  • Wrap the QuickBooks request in a Handle errors block, and on error tick a "Needs QBO sync" checkbox on the source record. A small scheduled pipeline can pick those up later and retry them.
  • If all of your QuickBooks work is one scheduled job anyway, put steps A through C at the start of that job instead of in a separate pipeline. It's still one refresher, and the access token is always brand new when the work runs. Just don't also keep a separate refresh pipeline.

Turn off the extra refreshers

Go to My pipelines and find every pipeline that calls oauth.platform.intuit.com. There should be exactly one. In the thread's case, that meant turning off the 12-hour pipeline and keeping the hourly one.

Also look for places a token might be hard-coded: a refresh token typed directly into a Make Request body, an old token in a different table, or a copy of the pipeline someone made while testing. Any of these will work for a day and then fail.

Troubleshooting invalid_grant

If the refresh step shows 400 with invalid_grant in the activity log, work through these in order:

  1. An old refresh token was sent. This is the common one. Check the record's Last Refreshed At against the activity log. Did step C run on every successful refresh, and does it map refresh_token from the response? Is any other pipeline refreshing? Is the token typed into a step instead of mapped from the record?
  2. The value was mangled. Look for leading or trailing spaces, line breaks from pasting, or a field too short for the value.
  3. It's the wrong app or environment. A refresh token from a sandbox app won't work with production keys, and a token from one app won't work with another app's client ID.
  4. The connection was revoked or replaced. If someone disconnected the app in QuickBooks, or authorized it again (in the Playground, for example) and didn't paste the new tokens into the record, the stored token is dead.
  5. The refresh token expired. It wasn't used for 100 days, or it hit the five-year limit. Refresh Token Expires At tells you which.
  6. Intuit is having problems. Check Intuit's developer status page. During an outage, wait it out. Don't reconnect.

A different error tells you something else. invalid_client (usually a 401) means the client ID or secret in step B's Basic authentication is wrong. A 401 from the QuickBooks API itself, as opposed to the token endpoint, usually just means the access token expired. See the hourly gap above.

Reconnecting after a failure

Once a refresh token is really dead, no pipeline change will bring it back. You need one new authorization:

  1. Fix whatever caused it first, usually turning off the second refresher.
  2. Repeat Step 2 to get a new access token and refresh token, and paste them into the QBO Connection record.
  3. Set Last Refresh Result back to OK.
  4. Run the refresh pipeline once by hand and confirm the record updates and Token Status shows OK.
  5. Check the record again the next day, after the first rotation. If Last Refreshed At keeps moving and Token Status still says OK, it's working.

After that it should keep going on its own until the five-year limit, as long as one pipeline does the refreshing and it saves both tokens every time.