Skip to content

Shypset API

Start a security scan with one request, poll until it is done and read the findings and a ready-made fix prompt as JSON. Built for your scripts, your CI and the AI agent that writes your code.

Version
v1
Auth
Bearer API key
Format
JSON

Overview#

The API does what you do in the app: start a scan of a URL, read the report and get fix prompts. It works with a personal key of your account, so a request acts as you and uses your plan: the same scan quota, the same report and the same locked or open prompts as in the app.

  • Works on every plan, including Free with its 3 scans a month. Scans started through the API and in the app share one quota.
  • The key belongs on a server or in a CI secret. Never put it in a website, mobile app or anything that runs on a user’s device: the API sends no CORS headers, so browsers can’t call it anyway, and a leaked key lets anyone scan on your account.
  • Every scan is the same non-intrusive external check as in the app. Deeper checks run only on a domain you verified in the app.

Base URL: https://dqyrveqnsggmckcabgmm.supabase.co/functions/v1/api-v1. All paths below start with /v1.

Quickstart#

Create a key, then start a scan, poll it and get a fix prompt. $SCAN_ID is the scan_id from the first answer.

export SHYPSET_API_KEY="shp_live_<your key>"
BASE=https://dqyrveqnsggmckcabgmm.supabase.co/functions/v1/api-v1

# 1. Start a scan
curl -sS -X POST "$BASE/v1/scans" \
  -H "Authorization: Bearer $SHYPSET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}'
# -> 202 {"scan_id": "...", "status": "queued", "poll_url": "..."}

# 2. Poll every 3-5 seconds until status is "completed" or "failed"
curl -sS "$BASE/v1/scans/$SCAN_ID" \
  -H "Authorization: Bearer $SHYPSET_API_KEY"

# 3. One fix prompt for the open issues, written for your AI tool
curl -sS "$BASE/v1/scans/$SCAN_ID/fix-prompt?target=cursor" \
  -H "Authorization: Bearer $SHYPSET_API_KEY"

Authentication#

Create a key in the app: Settings → API keys. The full key is shown once, right after you create it; we keep only its hash. You can have up to 5 active keys, for example one per CI or agent, and revoke any of them there. A revoked key stops working right away. When you create a key, choose when it expires: in 7, 30 or 90 days, in 1 year, or never. An expired key stops working too and no longer counts toward the 5.

Send the key in the Authorization header of every request:

Header
Authorization: Bearer shp_live_<your key>

Keep it in an environment variable such as SHYPSET_API_KEY or in your CI’s secrets, not in code. A missing, malformed, unknown, revoked or expired key always gets the same answer: 401 invalid_token.

Requests and errors#

  • Bodies are JSON (Content-Type: application/json), at most 16 KB.
  • Every answer is JSON with Cache-Control: no-store.
  • An error is always an error object with a stable code to branch on and a human-readable message; some errors add fields such as limit or upgrade_url.
Error
{
  "error": {
    "code": "invalid_url",
    "message": "Invalid URL format"
  }
}

Any endpoint can answer:

StatusCodeMeaning
401invalid_tokenThe key is missing, malformed, unknown, revoked or expired: one answer for all of them. Comes with WWW-Authenticate: Bearer.
404not_foundUnknown path.
405method_not_allowedWrong method for this path. The Allow header lists the right ones. OPTIONS is a 405 too: the API does not answer CORS preflights.
429rate_limitedToo many requests a minute. Fields limit, retry_after_seconds and scope (token_read_minute, user_read_minute or user_scan_minute). Wait for Retry-After seconds.
500internal_errorSomething failed on our side. Safe to retry.
503auth_unavailablerate_limit_unavailableA dependency is down and the API fails closed instead of skipping a check. Retry-After: 5.

Rate limits and quotas#

Two kinds of limits apply.

LimitValueAnswer
Reads (every GET)60 a minute per key and 120 a minute per account429 rate_limited
Starting scans10 a minute per account429 rate_limited
Scans per planYour plan’s quota, the same as in the app429 free_monthly, pro_monthly, …

The per-minute limits use a sliding 60-second window. Every 429 has a Retry-After header in seconds: wait that long before the next try. A quota 429 also says when the quota resets (reset_at).

Endpoints

Start a scan#

POST/v1/scans

Starts a scan of a public URL and answers at once with its id; the scan runs in the background. It is the same scan as in the app: it counts against your plan’s monthly quota, and the deeper checks run only on a domain you verified in the app.

The answer carries poll_url (also sent as the Location header): poll it for the result.

Parameters

NameInDescription
urlstring · requiredbodyThe site to scan: a public http(s) URL on port 80 or 443.
project_iduuidbodyAttach the scan to one of your projects. The project’s domain must match the URL.
Idempotency-Keystringheader1–100 printable ASCII characters, no spaces. A retry with the same key returns the same scan instead of starting (and paying for) another one. See Idempotency.

Request

curl -sS -X POST "$BASE/v1/scans" \
  -H "Authorization: Bearer $SHYPSET_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: deploy-2026-11-08-1" \
  -d '{"url": "https://example.com"}'

Response

202 Accepted
{
  "scan_id": "3f6c1a52-8d0e-4b7a-9c21-5e4f0a9b7d13",
  "status": "queued",
  "poll_url": "https://dqyrveqnsggmckcabgmm.supabase.co/functions/v1/api-v1/v1/scans/3f6c1a52-8d0e-4b7a-9c21-5e4f0a9b7d13"
}
429 Plan quota (Free)
{
  "error": {
    "code": "free_monthly",
    "message": "You have used all 3 free scans this month. Upgrade to Pro for up to 200 scans a month.",
    "limit": 3,
    "reset_at": "2026-12-01T00:00:00+00:00",
    "upgrade_url": "https://www.shypset.com/pricing"
  }
}
status
A new scan is queued (waiting for a scanner) or scanning. An idempotent replay (Idempotent-Replayed: true) returns the current status of the scan it started, so it can also be completed or failed.
poll_url
Full URL of GET /v1/scans/{id} for this scan.

Errors

StatusCodeMeaning
400invalid_jsonThe body is not a JSON object.
400invalid_urlMissing or malformed URL, a private or local address, a port other than 80/443, or a domain that does not resolve.
400invalid_projectproject_id is not one of your projects, or its domain does not match the URL.
400tos_requiredYour account has not accepted the current Terms. Open the app once and accept them.
400invalid_idempotency_keyIdempotency-Key is not 1–100 printable ASCII characters.
403account_blockedThe account is blocked.
403domain_opted_outThe owner of this domain asked us not to scan it.
409idempotency_conflictThis Idempotency-Key was already used with a different body.
409idempotency_in_progressA request with this key is still being processed. Retry-After: 2.
413body_too_largeThe body is over 16 KB.
429free_monthlypro_monthlypro_dailyhostname_hourlysite_hourly_globalYour plan’s scan quota, the same codes and limits as in the app. Fields limit and reset_at, on Free also upgrade_url. Retry-After until the reset.
503scanner_pausedScanning is paused for maintenance. Retry-After: 600.
503profile_unavailableidempotency_unavailableplan_unavailablequota_unavailableproject_unavailableblocklist_unavailableA dependency of the quota or safety checks is down; no scan was started and nothing was charged. Retry later.

Get a scan: progress or the report#

GET/v1/scans/{id}

While the scan runs, the answer is its progress and a Retry-After: 3 header. When it is done, status is completed or failed and report holds the report exactly as the app shows it to your account.

A completed scan also carries previous_scan_id and diff: the comparison with the previous completed scan of the same hostname that you can see. Both are null when there is no previous scan.

Someone else’s scan answers 404, the same as a scan that does not exist.

Parameters

NameInDescription
iduuid · requiredpathThe scan_id from POST /v1/scans.

Request

curl -sS "$BASE/v1/scans/3f6c1a52-8d0e-4b7a-9c21-5e4f0a9b7d13" \
  -H "Authorization: Bearer $SHYPSET_API_KEY"

Response

200 While it runs (Retry-After: 3)
{
  "id": "3f6c1a52-8d0e-4b7a-9c21-5e4f0a9b7d13",
  "url": "https://example.com",
  "status": "scanning",
  "queue_position": null,
  "progress": {
    "modules_completed": 41,
    "modules_total": 108,
    "module_status": { "security_headers": "completed", "tls_grade": "completed" }
  }
}
200 Completed
{
  "id": "3f6c1a52-8d0e-4b7a-9c21-5e4f0a9b7d13",
  "url": "https://example.com",
  "status": "completed",
  "tier": "full",
  "report": {
    "scan_id": "3f6c1a52-8d0e-4b7a-9c21-5e4f0a9b7d13",
    "url": "https://example.com",
    "status": "completed",
    "error_message": null,
    "risk_score": 72,
    "total_findings": 9,
    "verified_checks_available": 0,
    "created_at": "2026-11-08T10:15:04.123456+00:00",
    "counts": { "critical": 1, "high": 1, "medium": 3, "low": 2, "info": 2 },
    "findings": [
      {
        "id": "c1d2e3f4-0a1b-4c5d-8e9f-a0b1c2d3e4f5",
        "severity": "critical",
        "category": "secrets",
        "title": "OpenAI API Key Found in JavaScript",
        "description": "...",
        "evidence": "sk-proj-****",
        "remediation": "..."
      }
    ],
    "...": "more report sections"
  },
  "previous_scan_id": "9b2e7d40-1c5a-4f3e-8a6b-0d7c2e1f4a58",
  "diff": {
    "new": 1,
    "fixed": 3,
    "still_open": 8,
    "new_by_severity": { "critical": 1, "high": 0, "medium": 0, "low": 0, "info": 0 }
  }
}
status
queued or scanning while it runs, then completed or failed.
queue_position
Place in the scan queue while queued (1 = next), otherwise null.
tier
full (Pro, or a Launch Pass for this domain) or teaser (Free). See Free plan.
report.risk_score
0–100, higher is safer. The letter grade (A+ to F) is in GET /v1/scans as grade.
report.counts
Findings per severity over the whole scan, on every plan.
report.findings
Sorted by severity, then id, so the order is stable between calls. Evidence is already masked: secrets are never returned in full.
report.verified_checks_available
How many deeper checks did not run because the domain is not verified. Verify it in the app (free on every plan).
diff
new, fixed and still_open findings since previous_scan_id, and new_by_severity. Counted over the findings your plan shows; on Free a finding that only dropped out of the top 5 is not counted as fixed.

Errors

StatusCodeMeaning
404not_foundNo such scan, or it is not yours.
500internal_errorThe report failed to load. Retry.

Get a fix prompt#

GET/v1/scans/{id}/fix-prompt

A prompt to paste into your AI tool (or to hand to your agent) that explains the issues and how to fix them, built by the same generator as the prompts in the report.

Without finding_id it covers the open issues of the scan, at most the 15 most severe (truncated says the rest was cut). With finding_id it covers that one finding.

Parameters

NameInDescription
iduuid · requiredpathThe scan id. The scan must be completed.
targetstringqueryThe tool the prompt is written for: lovable, bolt, v0, replit, cursor, windsurf, claude (Claude / ChatGPT) or generic. By default, the platform detected on the site.
finding_idstringqueryThe id of one finding from report.findings.

Request

curl -sS "$BASE/v1/scans/3f6c1a52-8d0e-4b7a-9c21-5e4f0a9b7d13/fix-prompt?target=claude" \
  -H "Authorization: Bearer $SHYPSET_API_KEY" | jq -r .prompt

Response

200 OK
{
  "scan_id": "3f6c1a52-8d0e-4b7a-9c21-5e4f0a9b7d13",
  "target": "claude",
  "scope": "all",
  "prompt": "...",
  "included": 7,
  "truncated": false,
  "locked_count": 0
}
403 Free plan, locked finding
{
  "error": {
    "code": "upgrade_required",
    "message": "On the Free plan only the most severe issue has a fix prompt. Upgrade to Pro, or buy a Launch Pass for this domain, to unlock all of them.",
    "upgrade_url": "https://www.shypset.com/pricing"
  }
}
scope
all (no finding_id) or single.
included
How many issues the prompt covers.
truncated
true when more than 15 issues were open and the rest were cut.
locked_count
Issues you can see in the report whose prompt your plan does not open (Free).

Errors

StatusCodeMeaning
400invalid_targettarget is not one of the values above.
400invalid_finding_idfinding_id is not a valid finding id.
403upgrade_requiredOn Free only the most severe issue has a prompt. Field upgrade_url.
404not_foundNo such scan, or it is not yours.
404finding_not_foundNo issue with this finding_id in the scan.
409scan_not_readyThe scan has not completed (field status). While it runs, Retry-After: 3; a failed scan never gets prompts.
409nothing_to_fixThe scan found no issues to fix.

List your scans#

GET/v1/scans

Your scan history, newest first: your own scans and the scans in your projects, the same list as the dashboard. Summary fields only; get a scan for its findings.

A page may hold fewer than limit items even when more exist. Keep requesting with cursor set to next_cursor until next_cursor is null. How far back it goes follows your plan’s history.

Parameters

NameInDescription
urlstringqueryOnly scans of this hostname: example.com or a full URL.
project_iduuidqueryOnly scans of one of your projects.
limitintegerquery1–50, default 20.
cursorstringquerynext_cursor of the previous page. Opaque.

Request

curl -sS "$BASE/v1/scans?url=example.com&limit=20" \
  -H "Authorization: Bearer $SHYPSET_API_KEY"

# next page
curl -sS "$BASE/v1/scans?url=example.com&limit=20&cursor=$NEXT_CURSOR" \
  -H "Authorization: Bearer $SHYPSET_API_KEY"

Response

200 OK
{
  "items": [
    {
      "id": "3f6c1a52-8d0e-4b7a-9c21-5e4f0a9b7d13",
      "url": "https://example.com",
      "status": "completed",
      "risk_score": 72,
      "grade": "B",
      "critical_count": 1,
      "high_count": 1,
      "medium_count": 3,
      "low_count": 2,
      "created_at": "2026-11-08T10:15:04.123456+00:00",
      "completed_at": "2026-11-08T10:16:12.654321+00:00"
    }
  ],
  "next_cursor": null
}
risk_score, grade
null until the scan has completed.
next_cursor
Pass it as cursor for the next page; null on the last page.

Errors

StatusCodeMeaning
400invalid_urlurl is not a hostname or a URL.
400invalid_projectproject_id is not one of your projects.
400invalid_limitlimit is not an integer from 1 to 50.
400invalid_cursorcursor is not a next_cursor value from this endpoint.

Polling#

A scan takes a while, so POST /v1/scans answers right away and you poll poll_url. Ask every 3–5 seconds: a running scan answers with Retry-After: 3, and polling faster only spends your read limit. Stop when status is completed or failed, and give up after a few minutes in your script rather than polling forever.

For history, follow next_cursor until it is null. A short page does not mean the last page.

Idempotency#

A network error after POST /v1/scans leaves you not knowing whether the scan started. Send an Idempotency-Key header (1–100 printable ASCII characters, no spaces), and retry with the same key and exactly the same body:

  • For 24 hours the same key returns the same scan with Idempotent-Replayed: true, and no second scan is charged.
  • The same key with a different body is 409 idempotency_conflict.
  • While the first request is still being processed: 409 idempotency_in_progress with Retry-After: 2.
  • A refused request (for example a quota 429) does not keep the key: retrying with it later can start the scan.

Keys are per account. Use something unique per intended scan, such as your deploy id.

Free plan#

On Free the API returns the same shortened report as the app (tier: "teaser"):

  • the 5 most severe findings, with titles and short descriptions but without evidence and remediation;
  • the full counts per severity, so you know how many issues there are;
  • a fix prompt only for the most severe issue; the others answer 403 upgrade_required, and locked_count tells how many are locked.

Pro, or a Launch Pass for the scanned domain, returns the full report (tier: "full") and every prompt. See plans.

MCP server#

Claude Code, Cursor and other MCP clients can use Shypset as tools: start a scan, read the report and get the fix prompt without leaving the agent. It is the same API with the same key, plan limits and answers, over MCP (Streamable HTTP, JSON responses) at https://dqyrveqnsggmckcabgmm.supabase.co/functions/v1/api-v1/mcp.

Claude Code: put your key in SHYPSET_API_KEY and run once. Your shell fills in the key, and Claude Code keeps it in its own config, not in your project.

Claude Code
claude mcp add --transport http shypset \
  https://dqyrveqnsggmckcabgmm.supabase.co/functions/v1/api-v1/mcp \
  --header "Authorization: Bearer $SHYPSET_API_KEY"

Cursor: add this to ~/.cursor/mcp.json (or .cursor/mcp.json in a project you don’t commit the key to). Cursor reads the key from the SHYPSET_API_KEY environment variable.

mcp.json
{
  "mcpServers": {
    "shypset": {
      "url": "https://dqyrveqnsggmckcabgmm.supabase.co/functions/v1/api-v1/mcp",
      "headers": {
        "Authorization": "Bearer ${env:SHYPSET_API_KEY}"
      }
    }
  }
}

Tools:

ToolArgumentsWhat it does
scan_urlurl, project_id?, idempotency_key?Starts a scan, like POST /v1/scans, and returns scan_id. A scan takes about 1–2 minutes and counts against your plan’s quota.
get_reportscan_idStatus while the scan runs; when it is done, the grade, counts per severity, findings and diff against the previous scan, as GET /v1/scans/{id} (Free: the same top 5 findings).
get_fix_promptscan_id, finding_id?, target?The fix prompt, as GET /v1/scans/{id}/fix-prompt (Free: only the most severe issue).
list_scansurl?, project_id?, limit?, cursor?Your recent scans, optionally of one site, as GET /v1/scans.

A refusal comes back as a tool error with the same code as the REST API, for example free_monthly, rate_limited, invalid_url or upgrade_required; wrong argument types are invalid_arguments. A missing or wrong key is HTTP 401 before any tool runs. Tool calls count against the same per-minute limits: scan_url as starting a scan, the others as reads. Then ask your agent, for example:

Prompt
Use the shypset MCP tools to scan https://example.com, wait for the report and fix
what it finds in this codebase, most severe first. Tell me which issues are locked on my plan.

If Claude Code shows the server as “needs authentication”, the key is missing or invalid: the server takes only the API key, there is no OAuth sign-in.

For AI agents#

If your agent supports MCP, the MCP server is the shortest way. Any other agent can call the API directly. Put your key in the SHYPSET_API_KEY environment variable of the agent’s shell, then paste this prompt into Claude Code, Cursor or any agent that can run curl:

Prompt
Scan my site with the Shypset API and fix what it finds.

- Docs: https://www.shypset.com/docs/api (OpenAPI: https://www.shypset.com/openapi/v1.yaml)
- Base URL: https://dqyrveqnsggmckcabgmm.supabase.co/functions/v1/api-v1
- My API key is in the SHYPSET_API_KEY environment variable. Send it as
  "Authorization: Bearer $SHYPSET_API_KEY". Never print it, never write it to a file
  and never put it in client-side code.
- My site: https://example.com

1. POST /v1/scans with {"url": "<my site>"} and an Idempotency-Key header.
2. Poll GET /v1/scans/{scan_id} every 5 seconds until status is "completed" or "failed".
3. GET /v1/scans/{scan_id}/fix-prompt?target=claude (use target=cursor in Cursor) and follow
   that prompt: fix the issues in this codebase, most severe first. Do not invent issues
   that are not in the report.
4. If you get 403 upgrade_required, or locked_count is above 0, fix what you can see and
   tell me which issues are locked on my plan.
5. Tell me what you changed for each finding. After I deploy, scan again and report
   diff.fixed and diff.new from GET /v1/scans/{scan_id}.

Agents and tools can read the machine-readable OpenAPI spec and the short llms.txt.

From CI#

There is no ready-made GitHub Action yet, but you can call the API from CI like this. The step scans your site after a deploy and fails the job when the scan finds new critical issues compared with the previous scan. Add your key as the SHYPSET_API_KEY repository secret; the step needs curl and jq, both on GitHub-hosted runners.

GitHub Actions
# .github/workflows/shypset.yml (a step after your deploy job)
- name: Shypset scan
  shell: bash
  env:
    SHYPSET_API_KEY: ${{ secrets.SHYPSET_API_KEY }}
    SITE_URL: https://example.com
  run: |
    set -euo pipefail
    BASE=https://dqyrveqnsggmckcabgmm.supabase.co/functions/v1/api-v1
    AUTH="Authorization: Bearer $SHYPSET_API_KEY"

    # A refusal (for example 429 free_monthly) fails the step with the API's error.
    RESP=$(curl -sS --fail-with-body -X POST "$BASE/v1/scans" -H "$AUTH" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: gh-${{ github.run_id }}-${{ github.run_attempt }}" \
      -d "{\"url\": \"$SITE_URL\"}") || { echo "::error::Scan not started: $RESP"; exit 1; }
    SCAN_ID=$(echo "$RESP" | jq -r .scan_id)
    [ -n "$SCAN_ID" ] && [ "$SCAN_ID" != null ] || { echo "::error::No scan_id: $RESP"; exit 1; }

    STATUS=queued
    SCAN='{}'
    for i in $(seq 1 120); do
      sleep 5
      SCAN=$(curl -sS --fail-with-body "$BASE/v1/scans/$SCAN_ID" -H "$AUTH") || continue
      STATUS=$(echo "$SCAN" | jq -r .status)
      case "$STATUS" in completed|failed) break ;; esac
    done

    [ "$STATUS" = completed ] || { echo "::error::Scan did not complete (status: $STATUS)"; exit 1; }
    echo "score=$(echo "$SCAN" | jq .report.risk_score)"

    # diff is null on the first scan of a site: nothing to compare, nothing new.
    NEW_CRITICAL=$(echo "$SCAN" | jq '.diff.new_by_severity.critical // 0')
    if [ "$NEW_CRITICAL" -gt 0 ]; then
      echo "::error::$NEW_CRITICAL new critical issue(s) since the previous scan"
      exit 1
    fi

Each run starts a scan and counts against your plan’s quota, so run it on deploys to production rather than on every push.

Get your API key

Create a key in Settings and start your first scan from the terminal.

Open Settings → API keys