{
 "info": {
  "name": "G-Scan API",
  "description": "G-Scan · Garia.Net — every endpoint the tool's own page uses, so anything the page can do a script can do too.\n\nSet the collection variables first: `baseUrl` (the machine and port) and, for an agent, `key`. Locally there is no key; an agent refuses every call without one. See api/API.md for how the key and the rights work.\n\nStart with **hello** — it tells you whether a key is required at all.",
  "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
 },
 "variable": [
  {
   "key": "baseUrl",
   "value": "http://127.0.0.1:7666",
   "description": "The interface on this machine. An agent listens on 7777 by default: http://server:7777"
  },
  {
   "key": "key",
   "value": "",
   "description": "The agent key. Empty for a local instance, which asks for none. Read one with: gscan.exe --show-key"
  },
  {
   "key": "jobId",
   "value": "1",
   "description": "Scan number. /api/scan returns it, /api/jobs lists them."
  },
  {
   "key": "path",
   "value": "C:\\Users",
   "description": "A path on the scanned machine. Backslashes, and Postman escapes them for you."
  },
  {
   "key": "sshPassword",
   "value": "",
   "description": "Password for the SSH scan with a password. Fill in the current value only, so it is not exported or synced with the collection."
  }
 ],
 "auth": {
  "type": "apikey",
  "apikey": [
   {
    "key": "key",
    "value": "X-GScan-Token",
    "type": "string"
   },
   {
    "key": "value",
    "value": "{{key}}",
    "type": "string"
   },
   {
    "key": "in",
    "value": "header",
    "type": "string"
   }
  ]
 },
 "item": [
  {
   "name": "1 · Is anyone there",
   "item": [
    {
     "name": "hello",
     "request": {
      "method": "GET",
      "url": {
       "raw": "{{baseUrl}}/api/hello",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "hello"
       ]
      },
      "description": "The one call that never needs a key, so you can always find out whether one is needed.\n\n    {\"auth\":true,\"server\":false,\"canQuit\":false}\n\n`auth` true means every other call needs the key. `canQuit` false means this is an agent or a service, which refuses to be shut down over HTTP — that belongs to services.msc."
     }
    },
    {
     "name": "targets",
     "request": {
      "method": "GET",
      "url": {
       "raw": "{{baseUrl}}/api/targets",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "targets"
       ]
      },
      "description": "What can be scanned on this machine: drives with their size and free space, the WSL distributions, the home folder, the machine name, the version, and whether the process runs elevated.\n\nPer drive there is `turbo` and `turboReason`. `turbo:false` with `\"noadmin\"` means the fast MFT reader is unavailable because the process lacks administrator rights — it will still scan, by walking the folders, which is slower. An agent runs as SYSTEM and therefore always reports `turbo:true` on NTFS."
     }
    },
    {
     "name": "browse",
     "request": {
      "method": "GET",
      "url": {
       "raw": "{{baseUrl}}/api/browse?p={{path}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "browse"
       ],
       "query": [
        {
         "key": "p",
         "value": "{{path}}"
        }
       ]
      },
      "description": "The folders directly inside a path, live from disk. This is what the folder picker uses; it needs no scan."
     }
    }
   ]
  },
  {
   "name": "2 · Scanning",
   "item": [
    {
     "name": "scan · start",
     "event": [
      {
       "listen": "test",
       "script": {
        "type": "text/javascript",
        "exec": [
         "// het scannummer bewaren, zodat de volgende verzoeken het kunnen gebruiken",
         "const r = pm.response.json();",
         "if (r.id) pm.collectionVariables.set('jobId', String(r.id));",
         "pm.test('scan gestart', () => pm.expect(r.ok).to.be.true);"
        ]
       }
      }
     ],
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/x-www-form-urlencoded"
       }
      ],
      "body": {
       "mode": "urlencoded",
       "urlencoded": [
        {
         "key": "path",
         "value": "C:\\",
         "description": "what to scan"
        },
        {
         "key": "kind",
         "value": "drive",
         "description": "drive | folder | unc | wsl | ssh"
        },
        {
         "key": "label",
         "value": "C:",
         "description": "the name in the list, free text"
        }
       ]
      },
      "url": {
       "raw": "{{baseUrl}}/api/scan",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "scan"
       ]
      },
      "description": "Starts a scan and returns immediately with its number:\n\n    {\"id\":1,\"ok\":true}\n\nScans run alongside each other, each with its own progress and result. Poll **jobs** until `state` is `done`.\n\n`kind` picks the route: `drive` and `folder` are local, `unc` is a network path, `wsl` is `\\\\wsl.localhost\\<distro>\\`, and `ssh` is `user@host:/path` — that one runs `find` over SSH on the far machine.\n\nThe test script on this request stores the number in `jobId`."
     }
    },
    {
     "name": "scan · a folder",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/x-www-form-urlencoded"
       }
      ],
      "body": {
       "mode": "urlencoded",
       "urlencoded": [
        {
         "key": "path",
         "value": "{{path}}"
        },
        {
         "key": "kind",
         "value": "folder"
        },
        {
         "key": "label",
         "value": "Users"
        }
       ]
      },
      "url": {
       "raw": "{{baseUrl}}/api/scan",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "scan"
       ]
      },
      "description": "A folder rather than a whole drive. The turbo reader only does whole NTFS volumes, so this always walks the folders — fine for a few hundred thousand files."
     }
    },
    {
     "name": "scan · over SSH",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/x-www-form-urlencoded"
       }
      ],
      "body": {
       "mode": "urlencoded",
       "urlencoded": [
        {
         "key": "path",
         "value": "root@server:/var",
         "description": "user@host:/path, or ssh://user@host:2222/path"
        },
        {
         "key": "kind",
         "value": "ssh"
        },
        {
         "key": "label",
         "value": "server /var"
        }
       ]
      },
      "url": {
       "raw": "{{baseUrl}}/api/scan",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "scan"
       ]
      },
      "description": "Runs over the `ssh` client that Windows ships with and asks the far machine once for `df` and `find`. Nothing is installed there.\n\nThis one signs in with a key: the key of the account the tool runs under, with `BatchMode=yes`, so a key that does not fit fails at once. For a password, see the next request.\n\nMind who that account is: started by hand it is you, and it uses your key in `%USERPROFILE%\\.ssh`. As a service it is SYSTEM, whose keys live in `C:\\Windows\\System32\\config\\systemprofile\\.ssh`. A key that works for you is not automatically there for the service.\n\nErrors come back in `error` on **jobs**: `sshDenied` (sign-in refused), `sshFailed:<n>` (ssh's exit code; 255 is unreachable, port closed or host key changed), `sshEmpty` (nothing found - does the path exist?), `sshTarget`, `sshMissing`.\n\nThe result's `meta` carries `volumeSize` and `volumeFree` from `df`: the size of that filesystem and what `df` calls available."
     }
    },
    {
     "name": "scan · over SSH with a password",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/x-www-form-urlencoded"
       }
      ],
      "body": {
       "mode": "urlencoded",
       "urlencoded": [
        {
         "key": "path",
         "value": "admin@server:/var",
         "description": "user@host:/path, or ssh://user@host:2222/path"
        },
        {
         "key": "kind",
         "value": "ssh"
        },
        {
         "key": "label",
         "value": "server /var"
        },
        {
         "key": "password",
         "value": "{{sshPassword}}",
         "description": "the password for that user; set the collection variable sshPassword"
        }
       ]
      },
      "url": {
       "raw": "{{baseUrl}}/api/scan",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "scan"
       ]
      },
      "description": "The same scan, signed in with a password instead of a key - for a host that has no key of yours.\n\nSet the collection variable `sshPassword` first, as its *current* value only, so it is not exported or synced with the collection. Postman URL-encodes the field, so `&`, `+` and `%` arrive intact; from curl, use `--data-urlencode`.\n\nThe password reaches `ssh.exe` through askpass - in the environment of that one ssh process, never on its command line - is not kept with the scan and never comes back from **jobs**. A wrong one gives `sshDenied`.\n\nThe tool does no TLS: against a server or an agent across an untrusted network, the password travels the way the key does."
     }
    },
    {
     "name": "jobs · progress",
     "request": {
      "method": "GET",
      "url": {
       "raw": "{{baseUrl}}/api/jobs",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "jobs"
       ]
      },
      "description": "Every scan with its state — `scanning`, `done`, `cancelled` or `error` — plus files, bytes, folders and elapsed seconds. Poll this while a scan runs; the counters move.\n\n`phase` is 1 reading, 2 summing, 3 finished."
     }
    },
    {
     "name": "cancel",
     "request": {
      "method": "GET",
      "url": {
       "raw": "{{baseUrl}}/api/cancel?id={{jobId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "cancel"
       ],
       "query": [
        {
         "key": "id",
         "value": "{{jobId}}"
        }
       ]
      },
      "description": "Asks a running scan to stop. It finishes the folder it is in and then gives up, so it is not instant."
     }
    },
    {
     "name": "forget",
     "request": {
      "method": "GET",
      "url": {
       "raw": "{{baseUrl}}/api/forget?id={{jobId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "forget"
       ],
       "query": [
        {
         "key": "id",
         "value": "{{jobId}}"
        }
       ]
      },
      "description": "Throws a finished scan away and frees its memory. A scan of a million files is not small; on an agent that serves several machines this is worth doing.\n\nIt is also how you leave a scan out of **duplicates across scans**, which takes every finished scan there is. A previous across-scans result is dropped along with it, because it counted that scan."
     }
    }
   ]
  },
  {
   "name": "3 · Reading a result",
   "item": [
    {
     "name": "result",
     "request": {
      "method": "GET",
      "url": {
       "raw": "{{baseUrl}}/api/result?id={{jobId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "result"
       ],
       "query": [
        {
         "key": "id",
         "value": "{{jobId}}"
        }
       ]
      },
      "description": "The whole outcome: `meta` (path, method, seconds, files, folders, denied, skipped, volume size and free space - from Windows for a local drive, from `df` on the far machine over SSH - and `bigFileBytes` — the threshold above which a file is listed separately), the pruned `tree`, the largest files, the totals per file type, and `advice`.\n\nThe tree is pruned on purpose: seven levels deep and only branches above a size threshold, because a drive with half a million folders would otherwise be unusable. For anything deeper, use **dirs**.\n\nEvery advice item is a code with numbers, never a sentence — the page turns it into text in the chosen language. `paths`, `pathBytes` and `pathFiles` run in step: the places the finding is summed over, largest first."
     }
    },
    {
     "name": "dirs · children of any folder",
     "request": {
      "method": "GET",
      "url": {
       "raw": "{{baseUrl}}/api/dirs?id={{jobId}}&p={{path}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "dirs"
       ],
       "query": [
        {
         "key": "id",
         "value": "{{jobId}}"
        },
        {
         "key": "p",
         "value": "{{path}}"
        }
       ]
      },
      "description": "The folders directly inside a path, with their totals, from the scan — at any depth, so also below where the tree in **result** was pruned.\n\n    {\"path\":\"...\",\"s\":162623234322,\"f\":1334,\"dirs\":[{\"n\":\"ns_ue.ddc\",\"s\":...,\"f\":...}],\"total\":2}\n\n`s` and `f` at the top are that folder's own size and file count; `dirs` are its children, largest first, capped at 500.\n\nA path that was not part of the scan gives `\"error\":\"notScanned\"` rather than an HTTP error — asking about a folder that does not exist is a fair question."
     }
    },
    {
     "name": "files · what is in a folder now",
     "request": {
      "method": "GET",
      "url": {
       "raw": "{{baseUrl}}/api/files?p={{path}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "files"
       ],
       "query": [
        {
         "key": "p",
         "value": "{{path}}"
        }
       ]
      },
      "description": "The files in one folder, read live from disk rather than from the scan. So this is current even when the scan is an hour old, and it needs no scan number.\n\nFolders are not in this list — use **dirs** for those."
     }
    },
    {
     "name": "ext · one file type",
     "request": {
      "method": "GET",
      "url": {
       "raw": "{{baseUrl}}/api/ext?id={{jobId}}&e=.dll",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "ext"
       ],
       "query": [
        {
         "key": "id",
         "value": "{{jobId}}"
        },
        {
         "key": "e",
         "value": ".dll",
         "description": "with the dot; (none) and (other) also exist"
        }
       ]
      },
      "description": "Where one file type piles up (`folders`) and its largest files (`files`). That first answer is usually the useful one: it turns \"37 GB of .safetensors\" into \"17.3 GB of it in Downloads\".\n\nDrawn from the list of large files, so types made up of many small files are thin here — the folder totals say more about those."
     }
    },
    {
     "name": "export · CSV",
     "request": {
      "method": "GET",
      "url": {
       "raw": "{{baseUrl}}/api/export?id={{jobId}}&what=advice",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "export"
       ],
       "query": [
        {
         "key": "id",
         "value": "{{jobId}}"
        },
        {
         "key": "what",
         "value": "advice",
         "description": "types | files | advice"
        }
       ]
      },
      "description": "CSV with a header line naming the machine, the path and the timestamp, and a UTF-8 byte order mark so Excel reads the accents. Handy for a scheduled task that scans a fleet and drops the results somewhere."
     }
    }
   ]
  },
  {
   "name": "4 · Acting",
   "item": [
    {
     "name": "delete · dry run first",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/x-www-form-urlencoded"
       }
      ],
      "body": {
       "mode": "urlencoded",
       "urlencoded": [
        {
         "key": "job",
         "value": "{{jobId}}"
        },
        {
         "key": "p",
         "value": "C:\\Users\\me\\AppData\\Local\\Temp\\something",
         "description": "repeat p for each path"
        },
        {
         "key": "dryrun",
         "value": "1"
        },
        {
         "key": "permanent",
         "value": "0"
        }
       ]
      },
      "url": {
       "raw": "{{baseUrl}}/api/delete",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "delete"
       ]
      },
      "description": "**Always do this first.** With `dryrun=1` nothing is touched; you get back `wouldDelete` and, more importantly, `refused` with a reason per path.\n\nEverything must sit inside the scanned root, and the guard list refuses system paths outright — Windows, Program Files, System Volume Information, $Recycle.Bin, Boot, Recovery, and a whole user profile — whether or not `permanent` is set. A refusal is not a failure; it is the tool declining."
     }
    },
    {
     "name": "delete · for real",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/x-www-form-urlencoded"
       }
      ],
      "body": {
       "mode": "urlencoded",
       "urlencoded": [
        {
         "key": "job",
         "value": "{{jobId}}"
        },
        {
         "key": "p",
         "value": "C:\\Users\\me\\AppData\\Local\\Temp\\something"
        },
        {
         "key": "dryrun",
         "value": "0"
        },
        {
         "key": "permanent",
         "value": "0",
         "description": "0 = recycle bin, 1 = gone for good"
        }
       ]
      },
      "url": {
       "raw": "{{baseUrl}}/api/delete",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "delete"
       ]
      },
      "description": "`permanent=0` sends things to the recycle bin, which is recoverable and the default for a reason. `permanent=1` does not.\n\nEvery deletion is written to a log in `%LOCALAPPDATA%\\G-Scan\\verwijderd.log`; **targets** returns its path as `logPath`. For a service that is under SYSTEM's profile, not yours.\n\nThe answer holds `deleted`, `bytes` and any `refused` entries."
     }
    },
    {
     "name": "reveal · open in Windows Explorer",
     "request": {
      "method": "GET",
      "url": {
       "raw": "{{baseUrl}}/api/reveal?p={{path}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "reveal"
       ],
       "query": [
        {
         "key": "p",
         "value": "{{path}}"
        }
       ]
      },
      "description": "Opens Windows Explorer at that path, on the machine the tool runs on. Useless against an agent — nobody is looking at that screen."
     }
    },
    {
     "name": "empty the Recycle Bin",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/x-www-form-urlencoded"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/emptybin",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "emptybin"
       ]
      },
      "description": "Empties the Recycle Bin and reports what that freed: `{\"ok\":true,\"bytes\":...,\"items\":...}`.\n\nThis is a separate call and not a delete, on purpose. `/api/delete` refuses `$Recycle.Bin` outright, and rightly so — underneath it lies an administration of where every file came from, and taking a file out of there by hand leaves that administration behind. Windows has its own command for this, and this is it.\n\nIt is final: nothing moves to the Recycle Bin, because that is where it already is."
     }
    },
    {
     "name": "empty the folders of a finding",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/x-www-form-urlencoded"
       }
      ],
      "body": {
       "mode": "urlencoded",
       "urlencoded": [
        {
         "key": "job",
         "value": "{{jobId}}"
        },
        {
         "key": "what",
         "value": "tempfiles",
         "description": "tempfiles or updates — no other finding is allowed"
        }
       ]
      },
      "url": {
       "raw": "{{baseUrl}}/api/cleanup",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "cleanup"
       ]
      },
      "description": "Empties the folders that one finding points at, and keeps the folders themselves.\n\n**You do not pass paths.** The server takes them from the scan result and checks them again against a whitelist of tails (`\\windows\\temp`, `\\appdata\\local\\temp`, `\\softwaredistribution\\download`), so nothing else can be slipped in from outside. Anything in use is skipped rather than forced.\n\nAnswer: `{\"ok\":true,\"deleted\":n,\"skipped\":n,\"folders\":n,\"bytes\":n}`. Deletion is permanent here: moving temporary files to the Recycle Bin would leave the space exactly where it was."
     }
    },
    {
     "name": "duplicates — check they really are",
     "request": {
      "method": "GET",
      "url": {
       "raw": "{{baseUrl}}/api/verifydups?id={{jobId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "verifydups"
       ],
       "query": [
        {
         "key": "id",
         "value": "{{jobId}}"
        }
       ]
      },
      "description": "Confirms the `duplicates` finding of one scan by reading the files.\n\nThe finding itself groups by name and exact size without reading a byte — fast, and a guess. On one real disk ten of sixteen suspects were not copies: game data padded to a fixed block size.\n\nReading happens in two rounds. First the head and tail (64 KB each) plus the size of every candidate; whatever already differs there is settled and is never read in full. Only what still matches is then hashed completely with SHA-256. On the same disk that took the reading from 29.4 GB down to 1.56 GB, and the wait from half a minute to one second.\n\nPer file you get `p` and `h`. Group by `h`: appearing more than once means genuinely identical. A `h` starting with `q:` is the cheap head-and-tail fingerprint and means *ruled out* — that file is nobody's copy. An empty `h` means it could not be read; leave that one alone rather than assuming."
     }
    },
    {
     "name": "duplicates across scans — start",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/x-www-form-urlencoded"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/dupsacross",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "dupsacross"
       ]
      },
      "description": "Starts the cross-scan search and returns at once: `{\"started\":true,\"running\":true}`.\n\nAcross drives the name is the wrong key — the same file often sits under another name somewhere else — so this groups the large files of every finished scan by exact size (free: the sizes are already in the results) and then reads within those groups. Poll the same URL with GET.\n\nReading is capped at 40 GB of candidates; `\"capped\":true` in the result says the picture is incomplete."
     }
    },
    {
     "name": "duplicates across scans — progress or result",
     "request": {
      "method": "GET",
      "url": {
       "raw": "{{baseUrl}}/api/dupsacross",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "dupsacross"
       ]
      },
      "description": "While it runs:\n\n    {\"running\":true,\"total\":31600000000,\"read\":16100000000,\n     \"files\":40,\"files_done\":23,\"percent\":50,\"phase\":2}\n\nThe percentage is real, because which bytes must be read is settled before the first one is. `phase` says which of the two rounds is running: **1** reads head and tail of every candidate to rule out what differs, **2** reads in full what survived that. The counters reset between the rounds, which is why the phase is worth showing — otherwise the bar appears to jump back to zero.\n\nWhen it is finished the same URL returns the groups: each with its hash, the size, and every path with the scan (`job`) it came from, plus `groups_count`, `wasted` and `read`."
     }
    }
   ]
  },
  {
   "name": "5 · The process itself",
   "item": [
    {
     "name": "elevate",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/elevate",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "elevate"
       ]
      },
      "description": "Restarts the tool with administrator rights so the turbo reader becomes available. This raises a consent dialog on the machine itself, so it is only meaningful for a local instance with someone in front of it.\n\nThe restarted instance takes over the same port, so an open tab keeps working. Already elevated gives `{\"ok\":true,\"already\":true}`."
     }
    },
    {
     "name": "quit",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/quit",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "quit"
       ]
      },
      "description": "Shuts the tool down. An agent or a service refuses this with `{\"error\":\"quitRefused\"}` — a service is stopped through services.msc or `sc stop GScanAgent`, where an administrator looks for it and where Windows records it.\n\nFor a stuck instance on the machine itself there is also `gscan.exe --stop`, which asks every running instance to close, including one running elevated."
     }
    }
   ]
  }
 ]
}
