API Reference · Your Data

Exposed Credentials

Breach-exposed credentials discovered for a scan's domain — which accounts appeared in which breach corpus, and whether a plaintext or hashed password was recovered. Enough to drive a password-reset campaign; nothing that can be replayed.

This endpoint is heavily redacted by design. The password itself — plaintext or hash — never leaves the database, and neither does breach PII (names, addresses, phone numbers, dates of birth, social profiles). What you get is the account identifier, the breach source, and existence flags.

GET
/v1/my/scans/{scan_id}/credentials

Cost: 1 credit per successful call · Scope: api:org

Why redaction, not a special scope

Breach data is the most sensitive dataset on the platform, so the redaction is enforced in the database, not left to the client: the password and the breach PII are never part of the response, whoever holds the key. Pair api:org keys with a per-key IP allowlist to limit the blast radius of a leaked credential.

Path parameters

scan_idstring (uuid)required

A scan id from GET /my/scans. The scan's counts.exposed_credentials tells you how many rows to expect.

Query parameters

limitintegeroptional

Page size, 1–200. Default 50. Out-of-range is a 400, not a silent clamp.

cursorstringoptional

Opaque pagination cursor from a previous page's next_cursor.

Request

curl "https://api.precursorintelligence.com/functions/v1/my/scans/f63285e2-9c78-4020-b2bb-5c8bd2ca51ed/credentials" \
  -H "Authorization: Bearer $PRECURSOR_API_KEY"

Response

200 OK
{
  "data": {
    "count": 2,
    "items": [
      {
        "id": "00e528ef-7eb4-5650-f20d-cd42714a584f",
        "email": "j.smith@acmecorp.com",
        "username": "jsmith",
        "breach_source": "Collection #1",
        "has_plaintext_password": true,
        "has_hashed_password": false,
        "hash_algorithm": null,
        "discovered_at": "2026-08-06T06:28:00+00:00",
        "last_verified": "2026-08-06T06:28:00+00:00"
      },
      {
        "id": "1c88d2aa-0f4e-4b7d-9a02-8d2b1f7f11a3",
        "email": "it-support@acmecorp.com",
        "username": null,
        "breach_source": "LinkedIn 2021",
        "has_plaintext_password": false,
        "has_hashed_password": true,
        "hash_algorithm": "bcrypt",
        "discovered_at": "2026-08-06T06:28:00+00:00",
        "last_verified": "2026-08-06T06:28:00+00:00"
      }
    ],
    "next_cursor": null,
    "has_more": false,
    "generated_at": "2026-08-18T09:30:00+00:00"
  },
  "meta": {
    "request_id": "8b16efb8-9a6c-4ace-b817-11cbee626d08",
    "api_version": "1.3.0",
    "credits": { "charged": 1, "remaining": 9995 }
  }
}

Response fields

data.items[].emailstring | nulloptional

The breached account. This plus breach_source is the actionable pair: which accounts to force-reset, and why.

data.items[].breach_sourcestring | nulloptional

The breach corpus the credential appeared in.

data.items[].has_plaintext_passwordbooleanoptional

A recoverable plaintext password exists in the breach data. Treat the account as compromised; the password itself is never returned.

data.items[].has_hashed_passwordbooleanoptional

A password hash exists (hash_algorithm says which kind). Weak algorithms (MD5, SHA-1, unsalted anything) should be treated as close to plaintext.

A scan that isn't yours looks exactly like one that doesn't exist

An unknown scan_id — or another organisation's — returns 200 OK with data set to null, and the credit is still charged. Take scan ids from GET /my/scans.

Errors

StatuserrorWhen
400invalid_scan_id, invalid_limit, invalid_cursorA parameter failed validation. Free — rejected before billing.
401invalid_authorizationMissing or malformed Authorization header.
401unauthorizedInvalid/revoked key, IP not allowlisted, or the key lacks the api:org scope.
402insufficient_creditsOrganisation credit balance is below 1.
405method_not_allowedOnly GET and OPTIONS are accepted.
500internalUnexpected server error (credit auto-refunded).

See Errors for the full error reference.