> ## Documentation Index
> Fetch the complete documentation index at: https://docs.periphery.exposed/llms.txt
> Use this file to discover all available pages before exploring further.

# Get an app

> Returns the full record for one hostname: its summary (the same
fields and values a search hit carries), the visible page `text`
and, on Plus, the masked `credentials`. The record is read live, so
it can be newer than a cached search page; when they differ, the
record wins.

It does not use a search from the monthly allowance, but shares the
one request per second budget. Use it to refresh an app's signed
URLs, which expire after one hour.

Only hosts a search can return have a record: a host Periphery has
never visited, or one it proved dead, answers `404 app_not_found`.
`HEAD` is allowed and answers like `GET` without a body; any other
method is answered `405` before the key is checked.




## OpenAPI

````yaml /openapi.yaml get /v1/apps/{hostname}
openapi: 3.1.0
info:
  title: Periphery API
  version: 1.0.0
  summary: Search Periphery's index of publicly reachable web apps as JSON.
  description: |
    The API returns the same search results and app records as the console.
    Every request needs an API key issued on the console's account page,
    sent as `Authorization: Bearer <key>` or `X-API-Key: <key>`, over HTTPS
    (plain HTTP is refused with `403 https_required`). API access is part of
    the Plus plan. Each search request counts against the account's monthly
    allowance (3,000 on Plus, shared with the console), and requests are
    limited to one per second per account and ten per second per address.

    A search hit is an `AppSummary`; an app record (`App`) is the same
    summary plus `text` and `credentials`. A hostname reads the same on both
    endpoints for every field they share.

    Every response, success or error, is `application/json` with
    `Cache-Control: no-store`. Every error body is
    `{"error": "<message>", "code": "<code>"}`: switch on `code`, which is
    stable; the message may change. New fields, codes and enum values (such
    as provider slugs) may be added at any time, so ignore what you don't
    know.

    Credential findings are only ever returned masked, and the raw page
    source the credential scan ran over is never served. `text` and
    `screenshotUrl` show what the page displayed when it was captured, so a
    secret printed on the page appears in them as shown. Treat responses as
    sensitive.
  contact:
    name: Periphery
    email: contact@periphery.exposed
    url: https://docs.periphery.exposed
servers:
  - url: https://api.periphery.exposed
    description: Production
security:
  - bearerAuth: []
  - apiKeyHeader: []
tags:
  - name: Search
    description: Search the apps collection (`GET /v1/apps?q=…`) and page through the hits.
  - name: Apps
    description: Read one app's full record (`GET /v1/apps/{hostname}`).
paths:
  /v1/apps/{hostname}:
    get:
      tags:
        - Apps
      summary: Get an app
      description: |
        Returns the full record for one hostname: its summary (the same
        fields and values a search hit carries), the visible page `text`
        and, on Plus, the masked `credentials`. The record is read live, so
        it can be newer than a cached search page; when they differ, the
        record wins.

        It does not use a search from the monthly allowance, but shares the
        one request per second budget. Use it to refresh an app's signed
        URLs, which expire after one hour.

        Only hosts a search can return have a record: a host Periphery has
        never visited, or one it proved dead, answers `404 app_not_found`.
        `HEAD` is allowed and answers like `GET` without a body; any other
        method is answered `405` before the key is checked.
      operationId: getApp
      parameters:
        - name: hostname
          in: path
          required: true
          description: >-
            The app's hostname, as returned by search. It is matched
            case-insensitively; a trailing dot is ignored; Unicode IDNs are
            accepted. The response carries the canonical lower-case ASCII form.
            A value that cannot be a hostname answers `404 app_not_found`.
          schema:
            type: string
          example: acme-dashboard.vercel.app
      responses:
        '200':
          description: The app record.
          headers:
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/App'
              examples:
                withFindings:
                  summary: With credential findings
                  value:
                    hostname: acme-dashboard.vercel.app
                    provider: vercel
                    status: 200
                    title: Acme dashboard
                    firstSeen: '2026-08-02T10:15:40Z'
                    lastSeen: '2026-10-30T14:03:11Z'
                    scanned: true
                    faviconHash: -247388890
                    faviconUrl: >-
                      https://s3.gra.io.cloud.ovh.net/periphery/favicons/acme-dashboard.vercel.app.ico?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=…&X-Amz-Date=20261105T091500Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&x-id=GetObject&X-Amz-Signature=…
                    screenshotUrl: >-
                      https://s3.gra.io.cloud.ovh.net/periphery/screenshots/acme-dashboard.vercel.app.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=…&X-Amz-Date=20261105T091500Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&x-id=GetObject&X-Amz-Signature=…
                    emails:
                      - ops@acme.com
                    hasVerifiedCreds: true
                    verifiedDetectors:
                      - Stripe
                    text: >-
                      Acme dashboard Sign in Forgot your password? Contact
                      ops@acme.com
                    credentials:
                      - detector: Stripe
                        verified: true
                        masked: sk_live_••••••••0000
                      - detector: Github
                        verified: false
                        masked: ghp_8Kz2••••••••Qw1f
                scannedClean:
                  summary: Captured, nothing found
                  value:
                    hostname: acme-com-staging.vercel.app
                    provider: vercel
                    status: 401
                    title: Acme (staging)
                    firstSeen: '2026-09-17T22:05:19Z'
                    lastSeen: '2026-10-29T08:41:02Z'
                    scanned: true
                    emails: []
                    hasVerifiedCreds: false
                    verifiedDetectors: []
                    text: Acme staging Sign in
                    credentials: []
                notCaptured:
                  summary: Visited, never captured
                  value:
                    hostname: acme-com-preview.vercel.app
                    provider: vercel
                    status: 0
                    title: ''
                    firstSeen: '2026-10-12T03:22:48Z'
                    lastSeen: '2026-10-28T19:10:05Z'
                    scanned: false
                    hasVerifiedCreds: false
                    text: ''
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/AppNotFound'
        '405':
          $ref: '#/components/responses/AppMethodNotAllowed'
        '429':
          $ref: '#/components/responses/RateLimited'
        '502':
          $ref: '#/components/responses/Unavailable'
        '503':
          $ref: '#/components/responses/Maintenance'
components:
  headers:
    CacheControl:
      description: Always `no-store`, on every response.
      schema:
        type: string
        const: no-store
    WWWAuthenticate:
      description: >-
        The Bearer challenge: `Bearer realm="api.periphery.exposed"`, plus
        `error="invalid_token"` when the key is unknown or rotated.
      schema:
        type: string
    RetryAfter:
      description: >-
        Seconds to wait before retrying: 1 after a rate limit, 2 when busy, 5
        when unavailable, 3600 during maintenance. Not sent with the monthly
        limit (it resets on the 1st, UTC) or a timeout (a retry within a minute
        answers the same `504`).
      schema:
        type: integer
  schemas:
    App:
      type: object
      description: >-
        An app record: the app's summary (the same fields and values a search
        hit carries) plus `text` and, on Plus, `credentials`.
      allOf:
        - $ref: '#/components/schemas/AppSummary'
        - type: object
          required:
            - text
          properties:
            text:
              type: string
              description: >-
                Visible text extracted at the last capture, up to 2,048 bytes;
                `""` when none. It shows the page as captured and can contain a
                secret in full.
            credentials:
              type: array
              items:
                $ref: '#/components/schemas/Credential'
              description: >-
                Plus only. Every credential finding, verified or not, up to 20
                per host, verified first, each masked. `[]` when the credential
                scan found nothing; omitted when no scan was stored. Only this
                list is masked: `text` and `screenshotUrl` show what the page
                displayed.
    AppSummary:
      type: object
      description: >-
        One app, as a search hit returns it, and the first part of every app
        record. Strings, numbers and booleans are always present; timestamps are
        omitted when the event never happened; lists are `[]` when Periphery
        looked and found nothing and omitted when it never looked; signed URLs
        and `faviconHash` are omitted when absent. No field is ever `null`.
      required:
        - hostname
        - provider
        - status
        - title
        - lastSeen
        - scanned
        - hasVerifiedCreds
      properties:
        hostname:
          type: string
          description: The app's hostname, lower-case ASCII, its stable identifier.
          example: acme-dashboard.vercel.app
        provider:
          type: string
          description: >-
            Provider slug, the value the `provider:` filter takes, such as
            `vercel`, `netlify`, `railway` or `cloudflare_pages`. An open enum:
            new providers are added over time, see the providers page.
          example: vercel
        status:
          type: integer
          description: >-
            HTTP status from the last render or fallback request that got an
            answer; 0 when none was recorded. A bot-wall page does not update
            it.
          example: 200
        title:
          type: string
          description: >-
            Page title at the last capture; `""` when the page had none or was
            never rendered.
        firstSeen:
          type: string
          format: date-time
          description: >-
            Time of the first visit that got any answer from the host, RFC 3339
            in UTC. It never changes once set. For apps indexed before October
            2026, the time Periphery indexed them. Omitted when the host has
            never answered.
        lastSeen:
          type: string
          format: date-time
          description: >-
            Time of Periphery's last visit that did not prove the host dead,
            whether or not the host answered. Not the capture time. RFC 3339 in
            UTC; always present.
        scanned:
          type: boolean
          description: >-
            True when Periphery captured the page and stored a snapshot (text,
            emails and credential scans). False for a host it only probed for
            liveness: then `emails`, `verifiedDetectors` and the snapshot URLs
            are absent.
        faviconHash:
          type: integer
          format: int32
          description: >-
            Shodan-compatible favicon hash (mmh3). Present whenever a favicon
            was stored, even when the hash is 0; omitted otherwise.
        faviconUrl:
          type: string
          format: uri
          description: >-
            Signed URL of the stored favicon, valid for one hour; `GET
            /v1/apps/{hostname}` returns a fresh one without using a search. An
            SVG favicon downloads as an attachment when opened directly. Omitted
            when no favicon was stored.
        screenshotUrl:
          type: string
          format: uri
          description: >-
            Signed URL of the stored screenshot, valid for one hour; `GET
            /v1/apps/{hostname}` returns a fresh one without using a search. It
            shows the page as captured and can show a secret in full. Omitted
            when there is no screenshot.
        emails:
          type: array
          items:
            type: string
          description: >-
            Email addresses found on the page, lower-cased and deduplicated, up
            to 100. `[]` when the page was scanned and had none; omitted when it
            was never scanned.
        hasVerifiedCreds:
          type: boolean
          description: >-
            True when at least one credential finding was verified, so
            `verifiedDetectors` is non-empty.
        verifiedDetectors:
          type: array
          items:
            type: string
          description: >-
            Names of the detectors with a verified finding, deduplicated
            case-insensitively and sorted. Each works as `creds:<name>`. `[]`
            when the credential scan found nothing verified; omitted when no
            credential scan was stored.
    Credential:
      type: object
      required:
        - detector
        - verified
        - masked
      properties:
        detector:
          type: string
          description: Name of the detector that matched.
        verified:
          type: boolean
          description: >-
            True when the scanner confirmed the credential with its issuer at
            capture time.
        masked:
          type: string
          description: |
            The credential with its middle hidden: up to 8 leading and 4
            trailing characters around a fixed-width mask, enough to tell
            which key it is. This list never returns the full string; the
            `text` and `screenshotUrl` show a secret only as the page
            itself displayed it.
          example: sk_live_••••••••0000
    Error:
      type: object
      required:
        - error
        - code
      properties:
        error:
          type: string
          description: Human-readable reason. It may change; switch on `code`.
        code:
          type: string
          description: >-
            Stable machine-readable reason. New codes may be added: treat an
            unknown code by its HTTP status.
          enum:
            - query_required
            - query_too_long
            - too_many_terms
            - page_out_of_range
            - api_key_missing
            - api_key_invalid
            - plan_required
            - https_required
            - app_not_found
            - not_found
            - method_not_allowed
            - ip_rate_limited
            - rate_limited
            - monthly_limit_reached
            - unavailable
            - busy
            - maintenance
            - timeout
  responses:
    Unauthorized:
      description: Missing, unknown or rotated API key. Not counted.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        WWW-Authenticate:
          $ref: '#/components/headers/WWWAuthenticate'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            missing:
              summary: No key header
              value:
                error: missing API key
                code: api_key_missing
            invalid:
              summary: Unknown or rotated key
              value:
                error: invalid API key
                code: api_key_invalid
    Forbidden:
      description: >-
        The account is on the Free plan, which has no API access; or the request
        came over plain HTTP, in which case the key travelled in clear text and
        should be rotated. Not counted.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            planRequired:
              summary: Free plan
              value:
                error: API access requires the Plus plan
                code: plan_required
            httpsRequired:
              summary: Plain HTTP
              value:
                error: >-
                  HTTPS required: use https://api.periphery.exposed. A key sent
                  over plain HTTP travelled in clear text: rotate it.
                code: https_required
    AppNotFound:
      description: >-
        `app_not_found`: no app under that hostname that search can return
        (never visited, proven dead, unknown, or not a valid hostname). This
        endpoint itself answers only `app_not_found`, whatever follows
        `/v1/apps/`. `not_found` is the generic answer for a path outside the
        API, such as `/v1/x`.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            appNotFound:
              summary: No such app
              value:
                error: app not found
                code: app_not_found
            notFound:
              summary: No such path
              value:
                error: not found
                code: not_found
    AppMethodNotAllowed:
      description: A method other than `GET` or `HEAD`. Checked before the key.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Allow:
          description: '`GET, HEAD`.'
          schema:
            type: string
            const: GET, HEAD
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: method not allowed
            code: method_not_allowed
    RateLimited:
      description: >-
        Too many requests from this address (`ip_rate_limited`) or this account
        (`rate_limited`, one per second, shared with search). Retry after
        `Retry-After`.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            ipRate:
              summary: Over ten requests per second from this address
              value:
                error: 'rate limit: too many requests from this address'
                code: ip_rate_limited
            rate:
              summary: Over one request per second from this account
              value:
                error: 'rate limit: 1 request per second'
                code: rate_limited
    Unavailable:
      description: >-
        Periphery could not answer: a database or search backend failure, or the
        service is down. The result is unknown, not empty. Retry after
        `Retry-After`. A search answered this after the quota check counts.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Periphery is temporarily unavailable, try again in a moment
            code: unavailable
    Maintenance:
      description: Periphery is down for maintenance. Retry after `Retry-After`.
      headers:
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: >-
              Periphery is temporarily unavailable for maintenance, try again
              later
            code: maintenance
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: '`Authorization: Bearer <key>`. Used when both headers are present.'
    apiKeyHeader:
      type: apiKey
      in: header
      name: X-API-Key
      description: The same key in an `X-API-Key` header.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.