Stuck with Pi-hole 6.0 API in Docker (2FA & App Password Enabled) — Getting 400 “Bad Request”

Hey everyone,

I’m running Pi-hole 6.0 in Docker (Docker Tag 2025.02.0, Core v6.0, FTL vDev, Web interface v6.0). I’m trying to fetch Pi-hole’s summary via the new /api endpoints, but I keep getting this error in my Docker logs:

WARNING: API: Bad request (The API is hosted at pi.hole/api, not pi.hole/admin/api

And in the browser or HTTP response I see:

{"error":{"key":"bad_request","message":"Bad request","hint":"The API is hosted at pi.hole/api, not pi.hole/admin/api"},"took":6.6518783569335938e-05}

I have 2FA turned on and an application password created. My app supports HTTP Basic Auth and can handle GET/POST, etc., but I’m confused about whether I should:

  1. Use Basic Auth in the URL (and if so, what’s the username? “pihole”? blank?)
  2. Or first call /api/auth to get a session token and then pass it along?

Context:

  • I moved from older Pi-hole calls like http://<ip:port>/admin/api.php?... to http://<ip:port>/api/..., but I still get 400 errors.
  • Docker logs confirm Pi-hole wants me at /api instead of /admin/api.
  • After calling /api/auth, it returns a JSON object with session.valid=true, but how do I use that in a simple “paste-in-browser” scenario or a direct GET from my app? sid ? csrf?

Basically, I need a straightforward way (or code snippet) to fetch the “summary” (e.g. GET /api/stats/summary) while Pi-hole 6.0 is protected by 2FA and an app password. Also, if Basic Auth is possible, do I literally type http://pihole:APP_PASSWORD@<ip:port>/api/stats/summary in my browser?

Thanks in advance for any guidance or examples—apologies if this is newbie stuff, but I’m stumped!

Maybe this helps

Adding onto this.

I was able to figure out how to create a POST request via curl, get my SID and then use that SID to disable blocking temporarily. However, this process is WAY more convoluted than it used to be.

I used to have a browser shortcut to disable blocking for 30 seconds. The way it is now, it doesn't seem like that it's possible, because it's now a 2 step process. 1st to get a sid, and 2nd use that command to issue a POST request (GET is not even an option anymore).

Maybe I'm missing something that would allow this process to be automated in an easier fashion, but I'm not seeing it!

You can create and use the 'app password' which is designed for applications that don't support a 2 step authentication process (or even 2 factor authentication).

Go to Settings > Web Interface > App password

Thanks so much for your prompt answer. I really appreciate the help. I did try using the SID approach, but it turns out that’s not a route we can take in our setup. However, I still need to figure out how to get the Pi-hole status via a POST or GET request using HTTP headers (or body).

I’ve been searching for an article or some docs that clearly show what needs to go in the JSON body or which header(s) to use for authentication. I also haven’t found any examples of how to pass the application password directly (like <ip:port>/api/stats/summary?sid= or something similar). I checked the Pi-hole FTL docs at https://ftl.pi-hole.net/development/docs/, but haven’t seen anything that matches exactly what I’m looking for.

Would you be able to point me in the right direction or share an example of the proper syntax for using an app password with Pi-hole 6.0? Thanks again for your time and help!

In PiHole 5 I used a token to quickly disable blocking for some seconds without open dashboard.
In 6 I also use 2FA, to quickly disable blocking now I created also an app password and wrote this short bash script :

#!/bin/bash

SID="$(curl -X POST "https://pihole.local/api/auth" --data '{"password":"<APP_PASSWORD>"}' | jq -r '.session.sid')"
curl -X POST "https://pihole.local/api/dns/blocking?sid=${SID}" --data '{"blocking": false,"timer": 15}'
curl -X DELETE "https://pihole.local/api/auth?sid=${SID}"

Keep in mind to change the app password and url.
Perhaps this helps? :slight_smile:

4 Likes

This topic was automatically closed 21 days after the last reply. New replies are no longer allowed.