# Stack Detector > Stack Detector is a REST API that detects the technologies a website is built with (frameworks, CMS, analytics, CDNs, payment processors, and 5,000+ more). Send a URL, get a typed JSON response. Authentication is a single header - `X-API-Key` - so there are no SDKs to install and no OAuth flow. ## API basics - **Base URL**: `https://api.stackdetector.com` - **Versioning**: All endpoints are prefixed with `/v1`. - **Transport**: HTTPS only. JSON request and response bodies. - **Authentication**: Pass an API key in the `X-API-Key` header on every request. - Live keys: `tsk_live_…` - consume your monthly scan quota. - Test keys: `tsk_test_…` - hit the same backend, never consume quota, never return `402 quota_exceeded`. Use them for development, CI, and integration tests. - **Key scopes**: `full` keys can call any endpoint. `read` keys are restricted to `GET` and return `403 forbidden` on writes. - **Metering**: Per scan, not per request-per-second. A successful `/v1/analyze` call = 1 scan, and each bulk-scan item that completes costs 1. Partial scans (`partial: true`) are never billed. `/v1/scans`, `/v1/scans/export`, `/v1/timeline`, and other read-only endpoints are free. - **Result cache**: Scan results are cached for 1 hour per URL. A cache hit still counts against quota on live keys and is served transparently - the API response shape is identical whether or not the result came from cache. (The `cached`/`cacheExpiresAt` fields are exposed only to first-party dashboard sessions, not to `X-API-Key` callers.) ## Quickstart ```bash curl "https://api.stackdetector.com/v1/analyze?url=https://stripe.com" \ -H "X-API-Key: tsk_test_..." ``` Response (abridged): ```json { "url": "https://stripe.com", "scannedAt": "2026-05-01T17:21:08.913Z", "partial": false, "partialReason": null, "warnings": [], "technologies": [ { "name": "React", "slug": "react", "categories": ["JavaScript framework"], "confidence": 100, "version": "18.3.1", "website": "https://react.dev" }, { "name": "Cloudflare", "slug": "cloudflare", "categories": ["CDN"], "confidence": 100 } ] } ``` ## Endpoints ### Scans - `GET /v1/analyze?url={url}` - Scan a single public URL. Returns the detected technology stack. Required query: `url` (https only; localhost and RFC1918 ranges rejected; `https://` is auto-prepended if missing). Response: `url`, `scannedAt`, `partial`, `partialReason`, `warnings[]`, `technologies[]`. Each technology has `name`, `slug`, `categories[]`, `confidence`, and (when known) `version` and `website` - the `icon` field is first-party-only and is not returned to `X-API-Key` callers. ### Bulk scans Asynchronous batch scanning. Poll the job for progress, then export the result. - `POST /v1/bulk-scans` - Create a job. Body: `{ "urls": string[] }`, **max 5,000 URLs per request** (returns `413 invalid_urls` if exceeded; split into multiple jobs for more). Returns `{ "id", "totalUrls" }`. - `GET /v1/bulk-scans?page=&limit=` - List jobs (paginated, newest first; default limit 20, max 100). - `GET /v1/bulk-scans/{id}` - Retrieve a job and its per-URL items. Item status values: `pending | running | completed | failed | skipped`. - `POST /v1/bulk-scans/{id}/pause` - Pause a queued or running job. `409 not_pausable` if the job is in a different state. - `POST /v1/bulk-scans/{id}/resume` - Resume a paused job. `409 not_paused` if the job is not paused. - `POST /v1/bulk-scans/{id}/retry-failed` - Re-enqueue every failed item in the job. Returns `{ "retried": integer }`. - `POST /v1/bulk-scans/{id}/items/{itemId}/retry` - Re-enqueue a single failed item. - `DELETE /v1/bulk-scans/{id}` - Delete the job and its items (the underlying scans remain in scan history). - `GET /v1/bulk-scans/{id}/export` - Generate a CSV export of a finished job. Returns a JSON link object `{ "url", "expiresAt", "filename" }` (a time-limited download URL, not the file bytes - GET the `url` to fetch the CSV). `409 not_ready` if the job has not finished; `500 export_failed` if the export could not be generated. ### History Read your organization's full scan history - every scan triggered manually, via API, by bulk jobs, or by watches. - `GET /v1/scans` - Paginated list. Query params: `page`, `limit` (default 20, max 100), `sort` (`newest | oldest | most-techs | least-techs | slowest | fastest | url-asc | url-desc | user-asc | user-desc`), `q` (URL/domain match), `status` (`complete | partial`), `reason` (`blocked | timeout | unreachable`), `memberId`, `tech` (technology slug), `from`, `to` (ISO timestamp or `YYYY-MM-DD`). Returns `{ scans[], pagination, counts }`. - `GET /v1/scans/{id}` - Fetch a single scan with its full technology list. `404 not_found` if the scan doesn't belong to your organization. - `GET /v1/scans/export` - Download scan history as a CSV file (one row per scan: URL, domain, technology count, technology names, partial flag/reason, who ran it, timestamp). Accepts the same filter and `sort` query params as `GET /v1/scans` (no pagination); the export is capped at the 50,000 most recent matching scans. ### Insights - `GET /v1/timeline?url={url}&months={N}` - Up to 100 of the most recent scans your org has run for a URL within the last `months` (default 3, max 12). Returns `{ "scans": [{ "id", "url", "scannedAt", "technologies": [...] }] }`. ## Errors Errors share a consistent JSON envelope: ```json { "error": "slug", "message": "Human-readable explanation" } ``` Match on the stable `error` slug, not the message. Some scan errors also include the failing `url` field. | Status | Slug | Meaning | | ------ | ------------------- | --------------------------------------------------------------------------------------------- | | 400 | `bad_request` | Required parameter missing, malformed JSON body, or URL is not parseable / points private. | | 401 | `unauthorized` | API key missing, malformed, expired, or revoked. | | 402 | `quota_exceeded` | Monthly scan quota exhausted. Test-mode keys never trigger this. | | 403 | `forbidden` | Read-only key used on a write endpoint (POST, PATCH, DELETE). | | 404 | `not_found` | Resource (scan, bulk job, item) does not exist or is not visible to your organization. | | 409 | `not_ready` | Bulk CSV export was requested before the job finished. | | 409 | `not_pausable` | Bulk job is not in a pausable state (only queued or running jobs can be paused). | | 409 | `not_paused` | Bulk job is not paused, so it cannot be resumed. | | 413 | `invalid_urls` | Bulk request submitted too many URLs or contained URLs that failed validation. | | 422 | `detection_blocked` | Target site actively blocked the scanner (bot protection, WAF, captcha). | | 500 | `export_failed` | A bulk-scan export link could not be generated. Retry. | | 500 | `scan_failed` | Scan ran but failed unexpectedly. Inspect `message` and retry. | ## Pricing & plans Pricing is per organization, billed monthly (or annually for ~20% off). One scan = one URL = 1 credit, regardless of redirects or rendered sub-pages. Scans draw from a single org-wide monthly pool shared across the API, dashboard, bulk jobs, and watches. Blocked or partial scans (`partial: true`) are never billed. | Plan | Price (monthly) | Scans / month | API access | Notable limits / features | | ---------- | -------------------- | ------------- | ---------------- | -------------------------------------------------- | | Free | $0 (no card) | 30 | Test keys only | 3 seats, 30-day history, no CSV export, no watches | | Starter | $9.99 ($7.99/yr) | 1,000 | Live + test keys | 3 seats, unlimited history, CSV export, bulk scans | | Growth | $39.99 ($31.99/yr) | 5,000 | Live + test keys | Unlimited seats, watches, email + webhook alerts | | Enterprise | Custom | Custom | Live + test keys | Custom integrations, dedicated support | - **Live API access requires a paid plan.** Live keys (`tsk_live_…`) are available on Starter and up. Test keys (`tsk_test_…`) work on every account, including Free, and never consume quota - build and test against the real backend before subscribing. - **Top-ups:** $10 per 1,000 extra scans on any paid plan. Top-up credits never expire and are consumed only after the monthly quota is used up. - **Cancel anytime** - access continues until the end of the billing period. - The live, authoritative pricing page is https://stackdetector.com/pricing. Treat it as canonical if it ever disagrees with this table. ## Notes for agents - There is no SDK to import. Use whatever HTTP client your runtime ships with (`fetch`, `requests`, `net/http`). Set `X-API-Key` and you're done. - Slugs (in both technology results and error envelopes) are part of the API contract - they're stable across releases. Human-readable names and messages are not. - A scan can return `partial: true` when the target blocked full rendering; treat partial responses as best-effort. Partial responses are never cached. - `/v1/scans` and `/v1/scans/{id}` accept both API keys and dashboard sessions, so you can use them from a backend integration or from a browser session interchangeably. - Account-management surfaces (API key issuance, billing, monitoring/watches, webhook endpoints) are intentionally dashboard-only and not part of the public API. ## Links - [Agent skill (action-oriented guide for AI agents)](https://stackdetector.com/SKILL.md) - [Web reference (interactive, public)](https://stackdetector.com/api) - [Create an API key (requires sign-in)](https://stackdetector.com/api-keys) - [Pricing](https://stackdetector.com/pricing)