In 2023 Looker removed API 3.x. Instances upgraded past Looker 23.18 stopped serving /api/3.0/ and /api/3.1/, and Looker-hosted customers had been required to move to API 4.0 by that August. Three years on, we still find scripts, schedulers and Zapier-style integrations in client environments that nobody has run since, because the person who wrote them left and the failure was silent. This is the field guide we use to find and fix them.
How removal shows up
The symptoms are rarely labeled "API 3 removed":
- Login failures:
POST /api/3.1/loginreturns a 404 (or, on some configurations, a redirect to the login page), which older code often reports as "invalid credentials." - 404 on every endpoint: scripts that cached a token from somewhere else and hit
/api/3.1/queries/run/jsonget 404 rather than 401. - SDK exceptions:
looker_sdk.init31()was removed from the Python SDK; old pinned versions import but fail on login. - Empty deliveries: a cron job that exports a Look to CSV for finance "works" (exit 0 because nobody checked the status code) and has been emailing an empty file since 2023.
Start by grepping everything you can reach for api/3 and init31. Then look in Admin > Users for service users with API keys and check their last-login dates; a key that has not logged in since 2023 belongs to a broken integration or to nothing at all.
A diff of the endpoints people actually use
Most endpoint names survived; the differences are in the prefix, in typing, and in a few renames.
| Task | API 3.1 | API 4.0 |
|---|---|---|
| Login | POST /api/3.1/login | POST /api/4.0/login (same body) |
| Run an inline query | POST /api/3.1/queries/run/{format} | POST /api/4.0/queries/run/{format} (same body shape) |
| Run a Look | GET /api/3.1/looks/{id}/run/{format} | GET /api/4.0/looks/{id}/run/{format}; id is a string |
| List users | GET /api/3.1/users | GET /api/4.0/users; paginate with limit/offset |
| Dashboard render | POST /api/3.1/render_tasks/dashboards/{id}/{format} | POST /api/4.0/render_tasks/dashboards/{id}/{format} |
| Deploy LookML | POST /api/3.1/projects/{id}/deploy_ref_to_production | POST /api/4.0/projects/{id}/deploy_ref_to_production |
| Spaces | /api/3.1/spaces | Gone; use /api/4.0/folders |
| Content metadata | content_metadata | Unchanged, but fields are typed more strictly |
The two systematic changes: ids are strings in 4.0 ("42", not 42), and responses are strictly typed, so fields that used to come back as null or mixed types now have fixed types. Code that did int(look["id"]) keeps working; code that compared ids to integers silently stops matching.
Rewriting with the SDK
Replace hand-rolled requests code with the official SDK. It handles login, token expiry and typing, and its type hints surface the 4.0 changes as errors rather than runtime surprises.
Before (a typical 2021 script):
import requests
BASE = "https://looker.example.com:19999/api/3.1"
tok = requests.post(f"{BASE}/login", data={"client_id": CID, "client_secret": SECRET}).json()["access_token"]
h = {"Authorization": f"token {tok}"}
csv = requests.get(f"{BASE}/looks/42/run/csv", headers=h).text
After:
import looker_sdk
sdk = looker_sdk.init40() # reads LOOKERSDK_* env vars
csv = sdk.run_look(look_id="42", result_format="csv")
And for an inline query with typed models:
from looker_sdk import models40 as models
q = models.WriteQuery(
model="finance",
view="gl_entries",
fields=["gl_entries.posting_month", "gl_entries.net_amount"],
filters={"gl_entries.posting_date": "this year"},
limit="5000",
)
rows = sdk.run_inline_query(result_format="json", body=q)
For TypeScript, the equivalent is @looker/sdk with LookerNodeSDK.init40(). Keep the SDK version current; it tracks Looker releases and new endpoints land there first.
Hardening while you are in there
A migration is the right moment to fix the things that made the 2023 failure invisible.
One service user per integration. Create a user per script or system, with a role whose permission set is the minimum that works (often just access_data, see_looks, see_lookml_dashboards and schedule_look_emails) and a model set limited to the models it needs. When a key leaks, you know exactly what it could do and you can rotate one integration without touching the others.
Rotate and vault the keys. Generate fresh keys on the new service users, put them in Secret Manager, AWS Secrets Manager or your CI's secret store, and delete the old keys. Never in a looker.ini checked into Git.
Fail loudly. Every job should check the result (row count, non-empty file, HTTP status) and alert on failure. A Look that returns zero rows for finance is a page, not a log line.
Monitor the service users. A monthly query against System Activity for API users whose last query is older than 30 days finds dead integrations before the business does.
Prefer the warehouse for bulk. If a script is paging tens of thousands of rows through run_inline_query into another database, it should probably be a scheduled delivery to cloud storage, or the logic should be materialized in the warehouse where both systems can read it.
A one-week migration plan
- Day 1: inventory. Grep code, list API-keyed users, pull last-login dates, collect every schedule that targets a webhook or custom destination.
- Day 2: triage. Delete integrations nobody can name. For the rest, identify an owner and a test.
- Days 3 to 4: rewrite on the SDK, one service user each, secrets vaulted, assertions added.
- Day 5: run side by side against the expected outputs, rotate keys, delete the old users, document.
If that is more days than your team has, this is a package we deliver regularly as part of a Looker health check or on its own. Talk to us. For the full 4.0 walkthrough, see our refreshed tutorial on working with the Looker API and SDKs.