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

# Capture a website screenshot

> Capture a public webpage as a PNG using a scoped API key.

For server-to-server use, send a Pirehub API key with the `screenshot:capture`
scope in `x-pirehub-api-key` or as a bearer token. See
[Authentication](/api-reference/authentication) for credential handling.

See the [Website screenshots guide](/guides/screenshot) for capture options,
examples, caching, and limits.

The response contains `data.dataUrl` (a PNG data URI), the actual image `width`
and `height`, and a `cached` flag. Use `refreshCache: true` to request a fresh
capture. Cache hits remain subject to the API key's limits.

This scope grants access to `POST /api/v1/screenshot` only.


## OpenAPI

````yaml api-reference/openapi.json POST /api/v1/screenshot
openapi: 3.1.0
info:
  title: Pirehub by ntanduy API
  description: >-
    Scoped developer APIs from ntanduy for domain and IP intelligence, secured
    media processing, and Vietnamese bank QR utilities.
  version: 1.0.0
servers:
  - url: https://www.ntanduy.com
    description: Production
security: []
tags:
  - name: Screenshot tools
    description: Capture public webpages as PNG images.
  - name: Domains
    description: Domain registration, WHOIS, and DNS resolution intelligence.
  - name: IP information
    description: >-
      Normalized location, network, and timezone information for public IP
      addresses.
  - name: Video tools
    description: Video metadata and secure download-option extraction.
  - name: Payment utilities
    description: >-
      Credentialless utilities for generating interoperable bank-transfer
      artifacts.
  - name: Free subdomains
    description: >-
      Authenticated namespace registration, desired DNS state, asynchronous
      provider operations, and DNS propagation verification.
paths:
  /api/v1/screenshot:
    post:
      tags:
        - Screenshot tools
      summary: Capture a website screenshot
      description: >-
        Capture a public HTTP(S) webpage. API keys require screenshot:capture;
        authentication and key quotas are checked even on cache hits.
        First-party browser requests retain browser verification. Private
        network targets, URL credentials, and ports other than 80 or 443 are
        rejected. Capture processing has an 18-second deadline. Busy capture
        capacity returns 429; temporary capacity constraints return 503.
        Full-page website images are capped at 8192 pixels in height. Cached
        results may be evicted before their TTL.
      operationId: captureWebsiteScreenshot
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScreenshotCaptureRequest'
            example:
              url: https://example.com
              width: 1280
              height: 720
              fullPage: false
              device: desktop
              theme: light
              delay: 0
              refreshCache: false
      responses:
        '200':
          description: PNG screenshot in a JSON success envelope.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
            X-ApiKey-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-ApiKey-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-ApiKey-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            X-ApiKey-Monthly-Limit:
              $ref: '#/components/headers/MonthlyLimit'
            X-ApiKey-Monthly-Remaining:
              $ref: '#/components/headers/MonthlyRemaining'
            X-ApiKey-Monthly-Reset:
              $ref: '#/components/headers/MonthlyReset'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScreenshotSuccessResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '422':
          description: >-
            The page blocks capture or the requested content is too tall to
            capture as one image.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/UpstreamBadResponse'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
        '504':
          $ref: '#/components/responses/UpstreamTimeout'
      security:
        - pirehubOAuth:
            - screenshot:capture
        - pirehubApiKey: []
        - bearerApiKey: []
        - recaptchaToken: []
components:
  schemas:
    ScreenshotCaptureRequest:
      type: object
      required:
        - url
        - width
        - height
        - fullPage
      properties:
        url:
          type: string
          format: uri
          maxLength: 2048
          description: >-
            Public HTTP(S) URL without credentials; only ports 80 and 443 are
            allowed.
        width:
          type: integer
          minimum: 300
          maximum: 1920
        height:
          type: integer
          minimum: 168
          maximum: 4096
        fullPage:
          type: boolean
          description: Capture the full page instead of only the viewport.
        device:
          type: string
          enum:
            - desktop
            - mobile
          default: desktop
        theme:
          type: string
          enum:
            - light
            - dark
          default: light
        delay:
          type: integer
          enum:
            - 0
            - 2000
            - 5000
          default: 0
          description: Additional capture delay in milliseconds.
        refreshCache:
          type: boolean
          default: false
          description: Bypass the cached screenshot.
        showParentPost:
          type: boolean
          default: true
          description: 'X/Twitter post cards only: include the parent post when available.'
        includePostVariants:
          type: boolean
          default: false
          description: >-
            X/Twitter only: return S/M/L PNG layouts with parent-crop metadata
            for local editing without further requests. Increases initial
            response size. Variants include any available parent; the primary
            dataUrl still follows showParentPost.
        postWidth:
          type: string
          enum:
            - s
            - m
            - l
          default: s
          description: >-
            X/Twitter post card layout width: s=640, m=800, l=960 CSS pixels.
            Cards render at 3x density, scaled down if necessary to fit 8192
            pixels. Website viewport, device, theme, delay and fullPage options
            do not affect post cards; required fields must still be valid.
    ScreenshotSuccessResponse:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          const: true
        data:
          type: object
          required:
            - cached
            - dataUrl
            - width
            - height
          properties:
            cached:
              type: boolean
            dataUrl:
              type: string
              description: 'PNG data URI: data:image/png;base64,...'
            width:
              type: integer
              description: Actual output image width in pixels.
            height:
              type: integer
              description: >-
                Actual output image height in pixels; may differ from the
                viewport height.
            postVariants:
              type: object
              description: Present for X posts when includePostVariants is true.
              required:
                - s
                - m
                - l
              properties:
                s:
                  $ref: '#/components/schemas/ScreenshotPostVariant'
                m:
                  $ref: '#/components/schemas/ScreenshotPostVariant'
                l:
                  $ref: '#/components/schemas/ScreenshotPostVariant'
    ErrorResponse:
      type: object
      additionalProperties: false
      required:
        - success
        - error
        - data
      properties:
        success:
          type: boolean
          const: false
        error:
          $ref: '#/components/schemas/ErrorDetail'
        data:
          $ref: '#/components/schemas/ErrorData'
    ScreenshotPostVariant:
      type: object
      required:
        - dataUrl
        - width
        - height
        - parentHeight
        - topPadding
      properties:
        dataUrl:
          type: string
          description: PNG data URI including any available parent post.
        width:
          type: integer
        height:
          type: integer
        parentHeight:
          type: integer
          minimum: 0
          description: Pixels removed when hiding the parent; zero if absent.
        topPadding:
          type: integer
          minimum: 0
          description: >-
            Top padding in pixels. After removing the parent, repaint this
            padding with #0b0d10 to remove the thread connector.
    ErrorDetail:
      type: object
      additionalProperties: false
      required:
        - code
        - message
      properties:
        code:
          type: string
          description: Stable machine-readable error code.
        message:
          type: string
          description: Safe human-readable explanation.
    ErrorData:
      type: object
      description: >-
        Safe structured error metadata. Every JSON API error includes a
        correlation ID for support and log lookup.
      additionalProperties: true
      required:
        - requestId
      properties:
        requestId:
          type: string
          minLength: 1
          maxLength: 128
          pattern: ^[A-Za-z0-9._:-]+$
          description: >-
            Opaque correlation ID for this request. Include it when contacting
            support.
          example: 1704b073-5bf1-4d64-8f9a-3af95edb3bf1
    RateLimitErrorResponse:
      type: object
      additionalProperties: false
      required:
        - success
        - error
        - data
      properties:
        success:
          type: boolean
          const: false
        error:
          type: object
          additionalProperties: false
          required:
            - code
            - message
          properties:
            code:
              type: string
              enum:
                - RATE_LIMITED
                - MONTHLY_QUOTA_EXCEEDED
            message:
              type: string
        data:
          $ref: '#/components/schemas/RateLimitErrorData'
    RateLimitErrorData:
      type: object
      additionalProperties: false
      required:
        - requestId
      properties:
        reason:
          type: string
          enum:
            - per_minute
            - monthly_quota
          description: >-
            Present for API-key limits. Browser/IP and upstream limits may omit
            it.
        limit:
          type: integer
          minimum: 0
          description: Configured API-key limit for the applicable window.
        remaining:
          type: integer
          minimum: 0
          description: Requests remaining in the applicable API-key window.
        resetAt:
          type: string
          format: date-time
          description: ISO 8601 reset time for the applicable API-key window.
        retryAfter:
          type: integer
          minimum: 1
          description: Whole seconds to wait before retrying when supplied by the limiter.
        requestId:
          type: string
          minLength: 1
          maxLength: 128
          pattern: ^[A-Za-z0-9._:-]+$
          description: Opaque correlation ID for this request.
  headers:
    RequestId:
      description: >-
        Opaque correlation ID for this request. It matches `data.requestId` on
        JSON error responses.
      schema:
        type: string
        minLength: 1
        maxLength: 128
        pattern: ^[A-Za-z0-9._:-]+$
    RateLimitLimit:
      description: Maximum requests in the current per-minute window.
      schema:
        type: integer
    RateLimitRemaining:
      description: Requests remaining in the current per-minute window.
      schema:
        type: integer
    RateLimitReset:
      description: Per-minute window reset time as Unix seconds.
      schema:
        type: integer
    MonthlyLimit:
      description: Monthly request limit, or `unlimited`.
      schema:
        oneOf:
          - type: integer
          - type: string
            const: unlimited
    MonthlyRemaining:
      description: Requests remaining this month, or `unlimited`.
      schema:
        oneOf:
          - type: integer
          - type: string
            const: unlimited
    MonthlyReset:
      description: Monthly quota reset time as Unix seconds.
      schema:
        type: integer
    RetryAfter:
      description: Seconds to wait before retrying.
      schema:
        type: integer
        minimum: 0
  responses:
    ValidationError:
      description: The domain or request body is invalid.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: VALIDATION_ERROR
              message: Invalid domain format or missing required fields
            data:
              requestId: 1704b073-5bf1-4d64-8f9a-3af95edb3bf1
    UnauthorizedError:
      description: The API key or OAuth access token is missing or invalid.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            invalidKey:
              value:
                success: false
                error:
                  code: UNAUTHORIZED
                  message: A valid API key is required.
                data:
                  requestId: 1704b073-5bf1-4d64-8f9a-3af95edb3bf1
    ForbiddenError:
      description: >-
        The API key is revoked, expired, or lacks the required scope; or browser
        request verification is required or failed.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            missingScope:
              value:
                success: false
                error:
                  code: FORBIDDEN
                  message: This API key does not have permission for this endpoint.
                data:
                  requestId: 1704b073-5bf1-4d64-8f9a-3af95edb3bf1
            revokedKey:
              value:
                success: false
                error:
                  code: FORBIDDEN
                  message: This API key has been revoked.
                data:
                  requestId: 1704b073-5bf1-4d64-8f9a-3af95edb3bf1
            expiredKey:
              value:
                success: false
                error:
                  code: FORBIDDEN
                  message: This API key has expired.
                data:
                  requestId: 1704b073-5bf1-4d64-8f9a-3af95edb3bf1
            captchaFailed:
              value:
                success: false
                error:
                  code: CAPTCHA_FAILED
                  message: Request verification failed. Please try again.
                data:
                  requestId: 1704b073-5bf1-4d64-8f9a-3af95edb3bf1
            captchaRequired:
              value:
                success: false
                error:
                  code: CAPTCHA_REQUIRED
                  message: Request verification is required.
                data:
                  requestId: 1704b073-5bf1-4d64-8f9a-3af95edb3bf1
    RateLimitError:
      description: >-
        The API key exceeded its per-minute or monthly quota, the first-party
        browser request exceeded its per-IP limit, or an upstream provider
        throttled the request.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        X-ApiKey-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        X-ApiKey-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        X-ApiKey-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
        X-ApiKey-Monthly-Limit:
          $ref: '#/components/headers/MonthlyLimit'
        X-ApiKey-Monthly-Remaining:
          $ref: '#/components/headers/MonthlyRemaining'
        X-ApiKey-Monthly-Reset:
          $ref: '#/components/headers/MonthlyReset'
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RateLimitErrorResponse'
          examples:
            perMinute:
              summary: API-key per-minute limit exceeded
              value:
                success: false
                error:
                  code: RATE_LIMITED
                  message: Too many requests. Please try again later.
                data:
                  reason: per_minute
                  limit: 60
                  remaining: 0
                  resetAt: '2026-08-20T07:11:00.000Z'
                  retryAfter: 23
                  requestId: 1704b073-5bf1-4d64-8f9a-3af95edb3bf1
            monthlyQuota:
              summary: API-key monthly quota exhausted
              value:
                success: false
                error:
                  code: MONTHLY_QUOTA_EXCEEDED
                  message: Monthly API request quota has been exhausted.
                data:
                  reason: monthly_quota
                  limit: 10
                  remaining: 0
                  resetAt: '2026-09-01T00:00:00.000Z'
                  retryAfter: 1010458
                  requestId: 1f4f2b4a-da4f-47d2-be0e-f85ddaeab6d4
            browserPerIp:
              summary: First-party browser per-IP limit exceeded
              value:
                success: false
                error:
                  code: RATE_LIMITED
                  message: Too many requests. Please try again later.
                data:
                  retryAfter: 12
                  requestId: 1704b073-5bf1-4d64-8f9a-3af95edb3bf1
            upstreamRateLimit:
              summary: Upstream provider rate limited the lookup
              value:
                success: false
                error:
                  code: RATE_LIMITED
                  message: Too many requests. Please try again later.
                data:
                  requestId: 1704b073-5bf1-4d64-8f9a-3af95edb3bf1
    InternalError:
      description: An unexpected internal error occurred.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: INTERNAL_ERROR
              message: Something went wrong. Please try again later.
            data:
              requestId: 1704b073-5bf1-4d64-8f9a-3af95edb3bf1
    UpstreamBadResponse:
      description: An upstream provider returned an invalid or unusable response.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: UPSTREAM_BAD_RESPONSE
              message: Bad gateway. The upstream service returned an invalid response.
            data:
              requestId: 1704b073-5bf1-4d64-8f9a-3af95edb3bf1
    ServiceUnavailable:
      description: >-
        The requested service or one of its dependencies is temporarily
        unavailable.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            dependencyUnavailable:
              value:
                success: false
                error:
                  code: SERVICE_UNAVAILABLE
                  message: Service temporarily unavailable. Please try again later.
                data:
                  requestId: 1704b073-5bf1-4d64-8f9a-3af95edb3bf1
            quotaServiceUnavailable:
              value:
                success: false
                error:
                  code: SERVICE_UNAVAILABLE
                  message: API key quota service is temporarily unavailable.
                data:
                  retryAfter: 2
                  requestId: 1704b073-5bf1-4d64-8f9a-3af95edb3bf1
    UpstreamTimeout:
      description: The requested upstream dependency exceeded its deadline.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: UPSTREAM_TIMEOUT
              message: Gateway timeout. The upstream service did not respond in time.
            data:
              requestId: 1704b073-5bf1-4d64-8f9a-3af95edb3bf1
  securitySchemes:
    pirehubOAuth:
      type: oauth2
      description: >-
        OAuth 2.0 Authorization Code with PKCE, issued by Pirehub's Clerk
        authorization server. Request only the scopes required by the operation.
      flows:
        authorizationCode:
          authorizationUrl: https://clerk.ntanduy.com/oauth/authorize
          tokenUrl: https://clerk.ntanduy.com/oauth/token
          refreshUrl: https://clerk.ntanduy.com/oauth/token
          scopes:
            screenshot:capture: Capture a public webpage as a PNG image.
            domain:whois: Read normalized public WHOIS data.
            domain:dns: Read DNS records for a hostname.
            ip:lookup: Read allowlisted public IP information.
            video:extract: Read normalized metadata for a public video URL.
            bank-qr:generate: Generate a Vietnamese bank transfer QR payload and SVG.
            bank-qr:banks: Read the supported bank QR institution directory.
    pirehubApiKey:
      type: apiKey
      in: header
      name: x-pirehub-api-key
      description: A scoped Pirehub API key. Each operation documents its required scope.
    bearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: Pirehub API key
      description: A Pirehub API key beginning with `ph_live_`.
    recaptchaToken:
      type: apiKey
      in: header
      name: x-recaptcha-token
      description: >-
        A fresh one-time token generated by Pirehub's first-party browser
        reCAPTCHA flow.

````